rerender-lens 0.2.0 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +110 -1
- package/README.md +159 -9
- package/dist/budget-COBu7jBU.d.cts +64 -0
- package/dist/budget-LkNjGtRc.d.ts +64 -0
- package/dist/cli.cjs +458 -0
- package/dist/cli.cjs.map +1 -0
- package/dist/cli.d.cts +4 -0
- package/dist/cli.d.ts +4 -0
- package/dist/cli.js +452 -0
- package/dist/cli.js.map +1 -0
- package/dist/devtools-BkCct3cJ.d.cts +252 -0
- package/dist/devtools-CZebzpF6.d.ts +252 -0
- package/dist/index.cjs +832 -75
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +40 -336
- package/dist/index.d.ts +40 -336
- package/dist/index.js +816 -76
- package/dist/index.js.map +1 -1
- package/dist/notifiers-BGQtWKfX.d.cts +90 -0
- package/dist/notifiers-BjjGSHpp.d.ts +90 -0
- package/dist/playwright.cjs +199 -0
- package/dist/playwright.cjs.map +1 -0
- package/dist/playwright.d.cts +37 -0
- package/dist/playwright.d.ts +37 -0
- package/dist/playwright.js +186 -0
- package/dist/playwright.js.map +1 -0
- package/dist/relay.cjs +166 -0
- package/dist/relay.cjs.map +1 -0
- package/dist/relay.d.cts +41 -0
- package/dist/relay.d.ts +41 -0
- package/dist/relay.js +161 -0
- package/dist/relay.js.map +1 -0
- package/dist/rerender-lens.iife.js +2178 -0
- package/dist/setup.cjs +1638 -0
- package/dist/setup.cjs.map +1 -0
- package/dist/setup.d.cts +2 -0
- package/dist/setup.d.ts +2 -0
- package/dist/setup.js +1636 -0
- package/dist/setup.js.map +1 -0
- package/dist/types-BzUEVkxJ.d.cts +177 -0
- package/dist/types-BzUEVkxJ.d.ts +177 -0
- package/dist/vite.cjs +113 -0
- package/dist/vite.cjs.map +1 -0
- package/dist/vite.d.cts +84 -0
- package/dist/vite.d.ts +84 -0
- package/dist/vite.js +103 -0
- package/dist/vite.js.map +1 -0
- package/dist/vitest-setup.cjs +1368 -0
- package/dist/vitest-setup.cjs.map +1 -0
- package/dist/vitest-setup.d.cts +8 -0
- package/dist/vitest-setup.d.ts +8 -0
- package/dist/vitest-setup.js +1366 -0
- package/dist/vitest-setup.js.map +1 -0
- package/dist/vitest.cjs +1429 -0
- package/dist/vitest.cjs.map +1 -0
- package/dist/vitest.d.cts +52 -0
- package/dist/vitest.d.ts +52 -0
- package/dist/vitest.js +1420 -0
- package/dist/vitest.js.map +1 -0
- package/package.json +84 -4
- package/panel/panel.css +384 -0
- package/panel/panel.html +12 -0
- package/panel/panel.js +3287 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,7 +1,116 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
-
##
|
|
3
|
+
## 0.4.0
|
|
4
4
|
|
|
5
|
+
Highlights: the panel for any app with `npx rerender-lens panel`, Vitest and Playwright
|
|
6
|
+
integrations, bounded work per commit (large apps no longer freeze), a lighter panel, and the
|
|
7
|
+
injectable library split into three readable scripts. Details, newest first:
|
|
8
|
+
|
|
9
|
+
- **Scale test.** `examples/vite-react/scale.html?rows=N` renders N memoized cells sharing a
|
|
10
|
+
2,000-item context and a 4 MB typed-array prop; its readout shows the library's overhead and
|
|
11
|
+
the reports skipped by the cap. The e2e suite runs it with 3,000 rows. The panel shows the
|
|
12
|
+
skipped count in the status tooltip and as a banner.
|
|
13
|
+
- **`npx rerender-lens panel`.** A dependency-free relay (`rerender-lens/relay`) that serves the
|
|
14
|
+
panel and forwards messages between apps and panels over server-sent events and POST, so the
|
|
15
|
+
panel works for any app on any origin without the extension or Vite: Next.js, Webpack, a remote
|
|
16
|
+
dev box, a phone. `createDevtoolsNotifier({ relay })`, `window.__RERENDER_LENS_RELAY__`, or
|
|
17
|
+
`RERENDER_LENS_RELAY` / `NEXT_PUBLIC_RERENDER_LENS_RELAY` with `rerender-lens/setup`. Commands
|
|
18
|
+
(Settings, highlight, replay, clear) go back to the app; the panel re-attaches when an app
|
|
19
|
+
reloads.
|
|
20
|
+
- **`rerender-lens/vitest`.** `setupFiles: ['rerender-lens/vitest/setup']` collects every test
|
|
21
|
+
file's reports; the reporter prints the run's ranked fixes, enforces a budget file, and can
|
|
22
|
+
write a panel-compatible export. `setupRerenderLens(options, { afterAll })` for custom options
|
|
23
|
+
and `failFast`.
|
|
24
|
+
- **`rerender-lens/playwright`.** `installRerenderLens(page, options)` injects the library at
|
|
25
|
+
document start (the bundle now ships as `dist/rerender-lens.iife.js`); `pullReports`,
|
|
26
|
+
`clearReports`, `expectWithinBudget`.
|
|
27
|
+
- A second copy of the library (another bundle on the same page) no longer wraps the hook
|
|
28
|
+
again, which doubled every report.
|
|
29
|
+
- **Bounded work per commit** (large apps froze the page). Deep equality is memoized across the
|
|
30
|
+
components of one commit and gives up on values too large to walk (reported as `different`);
|
|
31
|
+
large Sets compare by identity. Serialization caps every array/object/Map/Set at 100 entries
|
|
32
|
+
(`…+N more`), every report at 20k nodes, treats typed arrays, `ArrayBuffer`, DOM nodes and
|
|
33
|
+
promises as leaves, and defaults to depth 4. A commit reports at most 200 tracked components
|
|
34
|
+
and stops after 25 ms; the rest is counted in `info().truncated` with a one-time warning.
|
|
35
|
+
Source locations are cached per element, the instance registry prunes on growth only, and
|
|
36
|
+
highlight/flash measure all nodes before drawing (at most 100 boxes).
|
|
37
|
+
- Reports are posted on `window` only once something says it listens (the extension's content
|
|
38
|
+
script, or `replay()`), so a page that runs the library without an open panel pays no
|
|
39
|
+
structured clone per report and wakes none of its own `message` listeners. `hello` and
|
|
40
|
+
`clear` still post; the BroadcastChannel and relay paths are unchanged.
|
|
41
|
+
- The injected library (extension) now runs with `includeState: false`; Settings can turn the
|
|
42
|
+
hook/context/state snapshots on per origin. Effect-loop detection no longer flags commits
|
|
43
|
+
caused by discrete input (typing, dragging). The toolbar badge repaints at most every 100 ms.
|
|
44
|
+
- **The panel keeps up.** Summary totals are incremental; the Offenders/Commits/Fixes lists and
|
|
45
|
+
the "best fix" stat update at most every 250 / 500 ms once 200 reports are buffered (the
|
|
46
|
+
tree and the stream stay live); commit analysis is memoized per commit and root causes are
|
|
47
|
+
found by index; each commit keeps at most 500 reports; buffer eviction is one splice per
|
|
48
|
+
frame. The content script sends one batched port message per task instead of one per
|
|
49
|
+
report. The DevTools panel retries the connect handshake with backoff (up to ~90 s) when a
|
|
50
|
+
page loads the library late, and the polling fallback clears the panel once per reset
|
|
51
|
+
instead of on every poll when the page overruns its buffer.
|
|
52
|
+
- The injectable library ships as three vendor scripts (`rerender-lens.core.js`,
|
|
53
|
+
`rerender-lens.engine.js`, `rerender-lens.js`) built by `scripts/build-vendor.mjs`; the
|
|
54
|
+
Playwright helper still gets one concatenated bundle.
|
|
55
|
+
- Removed: per-component notes and mute, share links (`panel.html?report=…`), the Offenders
|
|
56
|
+
column chooser (*Places*, *Last seen*), and the windowed Offenders/Fixes variant above 200 rows.
|
|
57
|
+
Offenders and Fixes always render plain rows; the tree and the stream stay virtualized. The
|
|
58
|
+
panel also drops its protocol-1 fallbacks: protocol 2 is the only one it speaks.
|
|
59
|
+
- Fix: after a navigation the panel's polling fallback stops until the new page answers `info`,
|
|
60
|
+
instead of evaluating into pages without the library every 500 ms.
|
|
61
|
+
|
|
62
|
+
## 0.3.0
|
|
63
|
+
|
|
64
|
+
Highlights: the panel without the extension (Vite plugin `panel: true`), the `rerender-lens/vite`
|
|
65
|
+
plugin and `rerender-lens/setup` entry, sessions with before/after comparison, custom hook names,
|
|
66
|
+
updaters and effect-loop detection, full hook/context/state snapshots, CI budgets and a CLI, the
|
|
67
|
+
side panel and window modes, and a UI refresh. Details, newest first:
|
|
68
|
+
|
|
69
|
+
- **Panel without the extension.** `rerenderLens({ panel: true })` serves the panel from the Vite
|
|
70
|
+
dev server at `/__rerender-lens/`; the app publishes on a same-origin `BroadcastChannel`
|
|
71
|
+
(`createDevtoolsNotifier({ channel })`) and the panel sends commands back over it. The `panel/`
|
|
72
|
+
directory ships in the package. `pages` limits which HTML pages get the setup script.
|
|
73
|
+
- **Overhead readout.** `info().overhead` reports the time the library spent inspecting commits
|
|
74
|
+
(total and worst commit); the panel shows it in the status tooltip.
|
|
75
|
+
- **Panel polish.** Offenders and Fixes are windowed above 200 rows (sticky header, sortable);
|
|
76
|
+
an Offenders column chooser adds *Places* and *Last seen*; the search box takes `~text` to
|
|
77
|
+
search prop, hook, context and state values; per-component notes and a *Mute* toggle (muted
|
|
78
|
+
components leave the Fixes ranking and the summary strip, persisted per origin); roles and
|
|
79
|
+
ARIA states on the tree, lists, toast and settings dialog, Escape closes Settings.
|
|
80
|
+
- **CI tooling.** `rankFixes` / `formatFixes`, `summarizeReports` / `compareSummaries`, and
|
|
81
|
+
`checkBudget` / `toBudget` / `assertWithinBudget` in the package; the collector gains
|
|
82
|
+
`fixes()`, `summary()` and `assertWithinBudget()`, and `assertNoAvoidable()` now ends with the
|
|
83
|
+
ranked fixes. A `rerender-lens` CLI (`fixes`, `summary --out`, `compare`, `budget --init`)
|
|
84
|
+
works on panel exports and session files and exits non-zero on regressions or violations.
|
|
85
|
+
- **Source context.** The report shows the lines around where the element was created (DevTools
|
|
86
|
+
resources or a fetch of the module in side-panel mode).
|
|
87
|
+
- **Shareable links.** *Copy link* puts the report into a `panel.html?report=…` URL (deflated when
|
|
88
|
+
the browser can); anyone with the extension opens it without the page.
|
|
89
|
+
- **Custom hook names, opt-in** (`resolveHookNames`, also in Settings): hook changes and
|
|
90
|
+
snapshots read `useCounter › useCart › useState#0`. Like React DevTools, the library re-runs a
|
|
91
|
+
component type once with a stand-in dispatcher and reads the custom hooks off the call stack;
|
|
92
|
+
results are cached per type and a failing replay yields no names.
|
|
93
|
+
- **Updaters.** Every report names the components that scheduled the commit (`updaters`, from
|
|
94
|
+
React's updater tracking), so the Commits view shows "set by <X>" even when X is untracked.
|
|
95
|
+
- **Effect loops.** A commit scheduled right after the previous one by a component that rendered
|
|
96
|
+
in it is flagged `effect-after-commit` with the offending commit id; the report explains the
|
|
97
|
+
effect → setState pattern. Suspense boundaries resolving are labelled `suspense-resolved`.
|
|
98
|
+
- **Store advice.** A `useSyncExternalStore` snapshot with equal contents now gets Redux
|
|
99
|
+
(`shallowEqual` / `createSelector`) or Zustand (`useShallow`) advice when the hook chain
|
|
100
|
+
identifies the store hook.
|
|
101
|
+
- **Instances.** Reports carry the element `key`; the tree can group by instance
|
|
102
|
+
(`<Row key="a">`, `<Row #12>`) via the Instances toggle.
|
|
103
|
+
- **`rerender-lens/vite`**: a Vite plugin that starts the library before React in dev, with the
|
|
104
|
+
DevTools notifier, from one line in `vite.config.ts`. **`rerender-lens/setup`**: a side-effect
|
|
105
|
+
entry for Next.js `instrumentation-client.ts`, Webpack entry arrays and the like.
|
|
106
|
+
- CI lints the Firefox package with `web-ext lint`; `extension/store/QA.md` is the manual
|
|
107
|
+
checklist for what no test reaches. A docs site builds from the READMEs (`npm run build:docs`)
|
|
108
|
+
and deploys to GitHub Pages; `npm run docs:media` captures its screenshots and GIF.
|
|
109
|
+
- **Full state on every report.** `hookState` (every `useState` / `useReducer` /
|
|
110
|
+
`useSyncExternalStore` value), `contexts` (every context the component reads) and, for class
|
|
111
|
+
components, `state`. The panel's report shows Hooks, State and Contexts in full, with the
|
|
112
|
+
changed entries as prev → next; the console prints them too. `includeState: false` turns it off
|
|
113
|
+
(also in the panel's Settings).
|
|
5
114
|
- **Sessions.** Record, use the app, Stop; apply a fix; record again. The Sessions view compares
|
|
6
115
|
two sessions: avoidable re-renders per component with deltas, totals, wasted time, and which
|
|
7
116
|
suggested fixes went away. Summaries persist per origin; exports include them.
|
package/README.md
CHANGED
|
@@ -52,8 +52,9 @@ Then pick one of two modes:
|
|
|
52
52
|
|
|
53
53
|
- **Inject the library** (no app code): tick *Inject the library* in the toolbar popup or in the
|
|
54
54
|
panel's Settings and reload. The extension loads rerender-lens into the page before React,
|
|
55
|
-
tracking every `React.memo` / `PureComponent` by default
|
|
56
|
-
Settings; the
|
|
55
|
+
tracking every `React.memo` / `PureComponent` by default, without the hook/context/state
|
|
56
|
+
snapshots (*Include state* in Settings turns them on; they are the costliest part on big
|
|
57
|
+
apps). Change what is tracked from Settings; the choice is saved per origin.
|
|
57
58
|
- **The page runs the library**: install the package and pass the DevTools notifier (see below).
|
|
58
59
|
This is the way to go when you also want console output or want to commit the setup.
|
|
59
60
|
|
|
@@ -77,6 +78,67 @@ npm i -D rerender-lens
|
|
|
77
78
|
|
|
78
79
|
Peer dependency: `react >= 16.8`. Tested with React 18 and 19.
|
|
79
80
|
|
|
81
|
+
## One-line setup per bundler
|
|
82
|
+
|
|
83
|
+
**Vite** (recommended for Vite users: no extension injection, no permissions, one line):
|
|
84
|
+
|
|
85
|
+
```ts
|
|
86
|
+
// vite.config.ts
|
|
87
|
+
import { rerenderLens } from 'rerender-lens/vite';
|
|
88
|
+
|
|
89
|
+
export default defineConfig({
|
|
90
|
+
plugins: [react(), rerenderLens({ trackAllMemoized: true })],
|
|
91
|
+
});
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
The plugin injects a module before your entry in dev (`vite build` is untouched) that calls
|
|
95
|
+
`init` with your options plus the DevTools notifier. Matchers can be strings or RegExps;
|
|
96
|
+
`devtools: false` skips the bridge, `applyInBuild: true` keeps it in builds, `pages: ['/']`
|
|
97
|
+
limits which HTML pages get it.
|
|
98
|
+
|
|
99
|
+
**No extension at all**: add `panel: true` and open `http://localhost:5173/__rerender-lens/` in a
|
|
100
|
+
second tab. The dev server serves the same panel the extension uses; the app publishes reports on
|
|
101
|
+
a same-origin `BroadcastChannel` and the panel sends settings and highlight commands back over it.
|
|
102
|
+
Panel state lives in `localStorage`. (`channel` renames the channel, `panel: '/some/path/'` moves
|
|
103
|
+
the mount.)
|
|
104
|
+
|
|
105
|
+
**Next.js** (App or Pages router): run it before React from the client instrumentation file:
|
|
106
|
+
|
|
107
|
+
```ts
|
|
108
|
+
// instrumentation-client.ts
|
|
109
|
+
import 'rerender-lens/setup';
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
**Webpack / Rspack / others**: put the setup entry first:
|
|
113
|
+
|
|
114
|
+
```js
|
|
115
|
+
entry: ['rerender-lens/setup', './src/index.tsx'],
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
`rerender-lens/setup` is a side-effect module that calls
|
|
119
|
+
`init({ trackAllMemoized: true, notifier: createDevtoolsNotifier() })` unless
|
|
120
|
+
`process.env.NODE_ENV === 'production'` or the page already runs the library. Use `configure()`
|
|
121
|
+
afterwards to change options, or the extension's Settings.
|
|
122
|
+
|
|
123
|
+
**The panel for any app, no extension, no Vite** (Next.js, Webpack, a remote dev box, a phone):
|
|
124
|
+
|
|
125
|
+
```sh
|
|
126
|
+
npx rerender-lens panel # http://127.0.0.1:4141/ serves the panel and relays messages
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
Point the app at it and open the printed URL in any browser:
|
|
130
|
+
|
|
131
|
+
```ts
|
|
132
|
+
init({ trackAllMemoized: true, notifier: createDevtoolsNotifier({ relay: 'http://127.0.0.1:4141' }) });
|
|
133
|
+
// or, with rerender-lens/setup: RERENDER_LENS_RELAY=http://127.0.0.1:4141 (NEXT_PUBLIC_RERENDER_LENS_RELAY for Next.js)
|
|
134
|
+
// or, before the app loads: window.__RERENDER_LENS_RELAY__ = 'http://127.0.0.1:4141'
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
The app streams commands in over server-sent events and posts hello, reports and replies back; the
|
|
138
|
+
panel connects the same way (`panel.html?relay=…`), so Settings, highlight and replay all work.
|
|
139
|
+
Several apps or panels can share one relay; the panel re-attaches when an app reloads.
|
|
140
|
+
`--port` and `--host` change where it listens (`--host 0.0.0.0` for another machine).
|
|
141
|
+
|
|
80
142
|
## Setup
|
|
81
143
|
|
|
82
144
|
```ts
|
|
@@ -136,6 +198,10 @@ Every update of a tracked component produces a `RenderReport`:
|
|
|
136
198
|
| `propChanges` | one entry per changed prop with `path`, `kind`, `prev`, `next` |
|
|
137
199
|
| `stateChanges` | class components: `this.state` diff |
|
|
138
200
|
| `hookChanges` | `useState`, `useReducer`, `useSyncExternalStore` and `useContext` values that changed; context entries carry `provider` (who renders it) and `changedKeys` / `totalKeys` for object values |
|
|
201
|
+
| `hookState`, `contexts`, `state` | current value of every state hook, every context read, and class `this.state` (off with `includeState: false`) |
|
|
202
|
+
| `updaters` | components that scheduled the commit (`setState`, dispatch), from React's updater tracking |
|
|
203
|
+
| `commitCause`, `afterCommit` | `effect-after-commit` (an effect of the previous commit set state) or `suspense-resolved` |
|
|
204
|
+
| `key` | the element's `key`, when it has one |
|
|
139
205
|
| `parent` | nearest ancestor that rendered in the same commit, and why |
|
|
140
206
|
| `owner` | component that created the element (dev builds) |
|
|
141
207
|
| `path` | component ancestry from the root |
|
|
@@ -184,6 +250,71 @@ test('typing in the search box does not re-render the grid rows', async () => {
|
|
|
184
250
|
In Vitest or Jest, call `ensureDevtoolsHook()` (or `init`) from a `setupFiles` entry so the hook
|
|
185
251
|
exists before `react-dom` is imported by your tests.
|
|
186
252
|
|
|
253
|
+
### Vitest, whole suite
|
|
254
|
+
|
|
255
|
+
Two config lines collect every avoidable re-render across the run and print the ranked fixes at
|
|
256
|
+
the end; a budget file turns it into a gate:
|
|
257
|
+
|
|
258
|
+
```ts
|
|
259
|
+
// vitest.config.ts
|
|
260
|
+
test: {
|
|
261
|
+
setupFiles: ['rerender-lens/vitest/setup'],
|
|
262
|
+
reporters: ['default', ['rerender-lens/vitest', { budget: 'rerender-budget.json', exportTo: 'rerender-lens.json' }]],
|
|
263
|
+
}
|
|
264
|
+
```
|
|
265
|
+
|
|
266
|
+
The setup entry starts the library (all memoized components, silent) in every test file and hands
|
|
267
|
+
the file's reports to the reporter; the reporter prints `rerender-lens: N reports, M avoidable`
|
|
268
|
+
with the fixes, fails the run on budget violations, and can write a panel-compatible export
|
|
269
|
+
(`Sessions > Import` in the extension). For other options, or per-test assertions, write your own
|
|
270
|
+
setup file:
|
|
271
|
+
|
|
272
|
+
```ts
|
|
273
|
+
import { afterAll } from 'vitest';
|
|
274
|
+
import { setupRerenderLens } from 'rerender-lens/vitest';
|
|
275
|
+
export const lens = setupRerenderLens({ include: [/^Grid/], failFast: true }, { afterAll });
|
|
276
|
+
// lens.collector.assertNoAvoidable() inside a test
|
|
277
|
+
```
|
|
278
|
+
|
|
279
|
+
### Playwright, real browser
|
|
280
|
+
|
|
281
|
+
```ts
|
|
282
|
+
import { installRerenderLens, pullReports, expectWithinBudget } from 'rerender-lens/playwright';
|
|
283
|
+
|
|
284
|
+
test('search does not re-render the grid', async ({ page }) => {
|
|
285
|
+
await installRerenderLens(page, { include: ['ProductRow'] }); // before goto: runs before React
|
|
286
|
+
await page.goto('/products');
|
|
287
|
+
await page.getByRole('searchbox').fill('abc');
|
|
288
|
+
expectWithinBudget(await pullReports(page), { '*': 0 }); // throws with the ranked fixes
|
|
289
|
+
});
|
|
290
|
+
```
|
|
291
|
+
|
|
292
|
+
`installRerenderLens` injects the same bundle the extension uses at document start, so it works on
|
|
293
|
+
any build the browser can load (production builds are flagged; names may be minified). Pass
|
|
294
|
+
`relay: 'http://127.0.0.1:4141'` to watch the run in the panel, `clearReports(page)` between
|
|
295
|
+
scenarios.
|
|
296
|
+
|
|
297
|
+
### Budgets and comparisons in CI
|
|
298
|
+
|
|
299
|
+
```ts
|
|
300
|
+
// allow known offenders, fail on anything new or worse
|
|
301
|
+
collector.assertWithinBudget(JSON.parse(readFileSync('rerender-budget.json', 'utf8')));
|
|
302
|
+
// produce the baseline once: writeFileSync('rerender-budget.json', JSON.stringify(toBudget(collector.reports)))
|
|
303
|
+
collector.fixes(); // ranked fixes, most re-renders removed first
|
|
304
|
+
collector.summary('after'); // a session summary, comparable with compareSummaries()
|
|
305
|
+
```
|
|
306
|
+
|
|
307
|
+
The `rerender-lens` CLI does the same with files from the extension (Export) or from `summary()`:
|
|
308
|
+
|
|
309
|
+
```sh
|
|
310
|
+
npx rerender-lens fixes export.json # ranked fixes
|
|
311
|
+
npx rerender-lens summary export.json --out before.json
|
|
312
|
+
npx rerender-lens compare before.json after.json # per-component deltas; exit 1 on regressions
|
|
313
|
+
npx rerender-lens budget export.json --init > rerender-budget.json
|
|
314
|
+
npx rerender-lens budget export.json rerender-budget.json # exit 1 when a component exceeds its budget
|
|
315
|
+
npx rerender-lens panel [--port 4141] [--host 127.0.0.1] # the panel + relay for any app (see above)
|
|
316
|
+
```
|
|
317
|
+
|
|
187
318
|
## Track a single component from the inside
|
|
188
319
|
|
|
189
320
|
Works anywhere without `init`, for example inside library packages or Storybook:
|
|
@@ -209,8 +340,12 @@ init({ trackAllMemoized: true, silent: true, notifier: createDevtoolsNotifier()
|
|
|
209
340
|
```
|
|
210
341
|
|
|
211
342
|
Each report is serialized (functions become `ƒ name`, elements `<Type>`, cycles cut) and posted
|
|
212
|
-
on `window` as `{ __rerenderLens: true, version: 2, type: 'report', payload }`.
|
|
213
|
-
|
|
343
|
+
on `window` as `{ __rerenderLens: true, version: 2, type: 'report', payload }`. Reports go on
|
|
344
|
+
`window` only after a listener announced itself (`{ __rerenderLensReady: true }`, which the
|
|
345
|
+
extension's content script posts) or after `replay()`, so a page nobody inspects pays no
|
|
346
|
+
structured clone per report. Values are bounded: 100 entries per array/object/Map/Set, depth 4
|
|
347
|
+
by default (`maxDepth`), 20k nodes per report. The last 300 reports are buffered. The bridge
|
|
348
|
+
at `window.__RERENDER_LENS_DEVTOOLS__` exposes:
|
|
214
349
|
|
|
215
350
|
| Method | |
|
|
216
351
|
| --- | --- |
|
|
@@ -236,10 +371,18 @@ disable(): void
|
|
|
236
371
|
isEnabled(): boolean
|
|
237
372
|
track(component, name?): component
|
|
238
373
|
ensureDevtoolsHook(): hook // create the global hook early (test setup files)
|
|
374
|
+
// 'rerender-lens/vite': rerenderLens(options), renderSetupModule(options)
|
|
375
|
+
// 'rerender-lens/setup': side-effect entry (init with defaults outside production)
|
|
376
|
+
// 'rerender-lens/relay': createRelayServer({ port?, host? }) — what `rerender-lens panel` runs
|
|
377
|
+
// 'rerender-lens/playwright': installRerenderLens(page, options?), pullReports(page), clearReports(page), expectWithinBudget(reports, budget)
|
|
378
|
+
// 'rerender-lens/vitest': setupRerenderLens(options?, { afterAll }), default reporter ({ budget?, exportTo?, limit? }); 'rerender-lens/vitest/setup'
|
|
239
379
|
useWhyRerender(name, values, options?)
|
|
240
|
-
createCollector(): { reports, avoidable, notifier, clear, assertNoAvoidable }
|
|
380
|
+
createCollector(): { reports, avoidable, notifier, clear, assertNoAvoidable, fixes, summary, assertWithinBudget }
|
|
381
|
+
rankFixes(reports), formatFixes(reports) // ranked fixes
|
|
382
|
+
summarizeReports(reports), compareSummaries(a, b), formatComparison(c), parseExport(json)
|
|
383
|
+
checkBudget(reports, budget), toBudget(reports), assertWithinBudget(reports, budget)
|
|
241
384
|
combineNotifiers(...notifiers): Notifier
|
|
242
|
-
createDevtoolsNotifier({ bufferSize?, target?, maxDepth?, flashAvoidable? }): Notifier
|
|
385
|
+
createDevtoolsNotifier({ bufferSize?, target?, maxDepth?, flashAvoidable?, channel?, relay? }): Notifier
|
|
243
386
|
getRenderers(), isProductionReact() // what react-dom registered on the DevTools hook
|
|
244
387
|
serializeOptions(o), deserializeOptions(o) // Options <-> JSON-safe form used by the bridge
|
|
245
388
|
VERSION
|
|
@@ -254,6 +397,8 @@ deepEqual(a, b), diffRecords(prev, next), classify(prev, next)
|
|
|
254
397
|
| `trackAllComponents` | `false` | track everything (noisy) |
|
|
255
398
|
| `include` / `exclude` | | display-name matchers |
|
|
256
399
|
| `trackHooks` | `true` | diff hook state and contexts |
|
|
400
|
+
| `includeState` | `true` (`false` when injected by the extension) | put every hook, context and class state value on each report; the costliest option on large trees |
|
|
401
|
+
| `resolveHookNames` | `false` | label hooks with the custom hooks that own them (`useCart › useState#0`); re-runs each component type once |
|
|
257
402
|
| `logAll` | `false` | print non-avoidable reports too |
|
|
258
403
|
| `silent` | `false` | never print; notifier still runs |
|
|
259
404
|
| `notifier` | | receives every `RenderReport` |
|
|
@@ -283,6 +428,10 @@ deepEqual(a, b), diffRecords(prev, next), classify(prev, next)
|
|
|
283
428
|
|
|
284
429
|
## Example app
|
|
285
430
|
|
|
431
|
+
`examples/vite-react/scale.html?rows=3000` is the scale test: thousands of memoized cells that all
|
|
432
|
+
re-render avoidably, sharing one large context value and a typed-array prop. Its readout shows
|
|
433
|
+
the library's own cost per commit and how many reports the per-commit cap skipped.
|
|
434
|
+
|
|
286
435
|
`examples/vite-react` has three deliberate bugs. From the repo root:
|
|
287
436
|
|
|
288
437
|
```sh
|
|
@@ -291,9 +440,10 @@ npm --prefix examples/vite-react install
|
|
|
291
440
|
npm run dev:example
|
|
292
441
|
```
|
|
293
442
|
|
|
294
|
-
`http://localhost:5199/` runs the library
|
|
295
|
-
`http://localhost:5199/plain.html`
|
|
296
|
-
extension's *Inject the library* mode.
|
|
443
|
+
`http://localhost:5199/` runs the library through the Vite plugin (console, extension, and the
|
|
444
|
+
built-in panel at `http://localhost:5199/__rerender-lens/`). `http://localhost:5199/plain.html`
|
|
445
|
+
is the same app without the library, for trying the extension's *Inject the library* mode.
|
|
446
|
+
`src/rerender-lens.ts` shows the manual setup for apps without the plugin.
|
|
297
447
|
|
|
298
448
|
## Migrating from why-did-you-render
|
|
299
449
|
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import { R as RenderReport } from './types-BzUEVkxJ.cjs';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Concrete fixes derived from reports, ranked by how many avoidable re-renders each one removes.
|
|
5
|
+
* The same logic drives the extension's Fixes view; here it is typed on `RenderReport` for tests,
|
|
6
|
+
* CI budgets and the CLI. Pure.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
type FixKind = 'memo' | 'useCallback' | 'useMemo' | 'useMemoElement' | 'children' | 'contextValue' | 'splitContext' | 'storeSnapshot' | 'bailout';
|
|
10
|
+
interface Fix {
|
|
11
|
+
kind: FixKind;
|
|
12
|
+
/** Where the change goes (the component that must be edited, or `<Ctx>.Provider`). */
|
|
13
|
+
owner: string;
|
|
14
|
+
/** The component whose re-render this removes. */
|
|
15
|
+
target: string;
|
|
16
|
+
prop: string | null;
|
|
17
|
+
label: string;
|
|
18
|
+
detail: string;
|
|
19
|
+
}
|
|
20
|
+
interface RankedFix extends Fix {
|
|
21
|
+
key: string;
|
|
22
|
+
/** Avoidable re-renders this fix removes. */
|
|
23
|
+
count: number;
|
|
24
|
+
components: Record<string, number>;
|
|
25
|
+
}
|
|
26
|
+
declare function fixesFor(r: RenderReport): Fix[];
|
|
27
|
+
declare const fixKey: (f: Fix) => string;
|
|
28
|
+
/** Every fix across `reports`, most re-renders removed first. */
|
|
29
|
+
declare function rankFixes(reports: RenderReport[]): RankedFix[];
|
|
30
|
+
/** Multi-line text for a test failure or a CI log. */
|
|
31
|
+
declare function formatFixes(reports: RenderReport[], limit?: number): string;
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Re-render budgets for CI: a JSON baseline of allowed avoidable re-renders per component, checked
|
|
35
|
+
* against a collector's reports (like a bundle-size budget). Pure; reading and writing the file is
|
|
36
|
+
* up to the test runner (`fs`), so this works in Node and in the browser.
|
|
37
|
+
*/
|
|
38
|
+
|
|
39
|
+
/** `{ "*": 0, "ProductRow": 3 }`: max avoidable re-renders per component; `*` is the default. */
|
|
40
|
+
type Budget = Record<string, number>;
|
|
41
|
+
interface BudgetViolation {
|
|
42
|
+
component: string;
|
|
43
|
+
avoidable: number;
|
|
44
|
+
allowed: number;
|
|
45
|
+
}
|
|
46
|
+
interface BudgetResult {
|
|
47
|
+
ok: boolean;
|
|
48
|
+
violations: BudgetViolation[];
|
|
49
|
+
/** Components with headroom left, so budgets can be tightened. */
|
|
50
|
+
slack: {
|
|
51
|
+
component: string;
|
|
52
|
+
avoidable: number;
|
|
53
|
+
allowed: number;
|
|
54
|
+
}[];
|
|
55
|
+
counts: Record<string, number>;
|
|
56
|
+
}
|
|
57
|
+
declare function avoidableCounts(reports: RenderReport[]): Record<string, number>;
|
|
58
|
+
declare function checkBudget(reports: RenderReport[], budget: Budget | number): BudgetResult;
|
|
59
|
+
/** A budget that exactly matches the current counts (the baseline to commit). */
|
|
60
|
+
declare function toBudget(reports: RenderReport[]): Budget;
|
|
61
|
+
/** Throws with the violations and the ranked fixes; use it from a test. */
|
|
62
|
+
declare function assertWithinBudget(reports: RenderReport[], budget: Budget | number): BudgetResult;
|
|
63
|
+
|
|
64
|
+
export { type Budget as B, type Fix as F, type RankedFix as R, type BudgetResult as a, type BudgetViolation as b, checkBudget as c, type FixKind as d, assertWithinBudget as e, formatFixes as f, avoidableCounts as g, fixKey as h, fixesFor as i, rankFixes as r, toBudget as t };
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import { R as RenderReport } from './types-BzUEVkxJ.js';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Concrete fixes derived from reports, ranked by how many avoidable re-renders each one removes.
|
|
5
|
+
* The same logic drives the extension's Fixes view; here it is typed on `RenderReport` for tests,
|
|
6
|
+
* CI budgets and the CLI. Pure.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
type FixKind = 'memo' | 'useCallback' | 'useMemo' | 'useMemoElement' | 'children' | 'contextValue' | 'splitContext' | 'storeSnapshot' | 'bailout';
|
|
10
|
+
interface Fix {
|
|
11
|
+
kind: FixKind;
|
|
12
|
+
/** Where the change goes (the component that must be edited, or `<Ctx>.Provider`). */
|
|
13
|
+
owner: string;
|
|
14
|
+
/** The component whose re-render this removes. */
|
|
15
|
+
target: string;
|
|
16
|
+
prop: string | null;
|
|
17
|
+
label: string;
|
|
18
|
+
detail: string;
|
|
19
|
+
}
|
|
20
|
+
interface RankedFix extends Fix {
|
|
21
|
+
key: string;
|
|
22
|
+
/** Avoidable re-renders this fix removes. */
|
|
23
|
+
count: number;
|
|
24
|
+
components: Record<string, number>;
|
|
25
|
+
}
|
|
26
|
+
declare function fixesFor(r: RenderReport): Fix[];
|
|
27
|
+
declare const fixKey: (f: Fix) => string;
|
|
28
|
+
/** Every fix across `reports`, most re-renders removed first. */
|
|
29
|
+
declare function rankFixes(reports: RenderReport[]): RankedFix[];
|
|
30
|
+
/** Multi-line text for a test failure or a CI log. */
|
|
31
|
+
declare function formatFixes(reports: RenderReport[], limit?: number): string;
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Re-render budgets for CI: a JSON baseline of allowed avoidable re-renders per component, checked
|
|
35
|
+
* against a collector's reports (like a bundle-size budget). Pure; reading and writing the file is
|
|
36
|
+
* up to the test runner (`fs`), so this works in Node and in the browser.
|
|
37
|
+
*/
|
|
38
|
+
|
|
39
|
+
/** `{ "*": 0, "ProductRow": 3 }`: max avoidable re-renders per component; `*` is the default. */
|
|
40
|
+
type Budget = Record<string, number>;
|
|
41
|
+
interface BudgetViolation {
|
|
42
|
+
component: string;
|
|
43
|
+
avoidable: number;
|
|
44
|
+
allowed: number;
|
|
45
|
+
}
|
|
46
|
+
interface BudgetResult {
|
|
47
|
+
ok: boolean;
|
|
48
|
+
violations: BudgetViolation[];
|
|
49
|
+
/** Components with headroom left, so budgets can be tightened. */
|
|
50
|
+
slack: {
|
|
51
|
+
component: string;
|
|
52
|
+
avoidable: number;
|
|
53
|
+
allowed: number;
|
|
54
|
+
}[];
|
|
55
|
+
counts: Record<string, number>;
|
|
56
|
+
}
|
|
57
|
+
declare function avoidableCounts(reports: RenderReport[]): Record<string, number>;
|
|
58
|
+
declare function checkBudget(reports: RenderReport[], budget: Budget | number): BudgetResult;
|
|
59
|
+
/** A budget that exactly matches the current counts (the baseline to commit). */
|
|
60
|
+
declare function toBudget(reports: RenderReport[]): Budget;
|
|
61
|
+
/** Throws with the violations and the ranked fixes; use it from a test. */
|
|
62
|
+
declare function assertWithinBudget(reports: RenderReport[], budget: Budget | number): BudgetResult;
|
|
63
|
+
|
|
64
|
+
export { type Budget as B, type Fix as F, type RankedFix as R, type BudgetResult as a, type BudgetViolation as b, checkBudget as c, type FixKind as d, assertWithinBudget as e, formatFixes as f, avoidableCounts as g, fixKey as h, fixesFor as i, rankFixes as r, toBudget as t };
|