rerender-lens 0.1.0 → 0.2.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 CHANGED
@@ -1,15 +1,104 @@
1
1
  # Changelog
2
2
 
3
+ ## Unreleased
4
+
5
+ - **Sessions.** Record, use the app, Stop; apply a fix; record again. The Sessions view compares
6
+ two sessions: avoidable re-renders per component with deltas, totals, wasted time, and which
7
+ suggested fixes went away. Summaries persist per origin; exports include them.
8
+ - **Provider attribution.** A `useContext` change now names the component that renders the
9
+ Provider and, for object values, which keys changed. When only some keys changed the reason
10
+ and the Fixes view say so and suggest splitting the context or selecting slices.
11
+ - **Children diffs.** Elements are serialized with their props, so a changed `children` prop
12
+ shows the differing leaf (`children[0].props.label`); re-created children get their own advice
13
+ and fix snippet (memoize or hoist them).
14
+ - **Commit priority.** Every report carries the priority React gave the commit (discrete input,
15
+ continuous input, transition / async, low, idle); the Commits list and report show it. The
16
+ bridge `info()` also counts scheduled roots vs commits.
17
+ - Not done: custom hook names for hook changes. React DevTools gets them by re-running the
18
+ component with a fake dispatcher; doing that from a commit hook is too invasive.
19
+ - The panel outside DevTools: **Open side panel** shows it in Chrome's side panel next to the
20
+ page, **Open in window** in its own window (toolbar popup, or the new buttons in the DevTools
21
+ panel). Both use the background relay plus `chrome.scripting` instead of DevTools APIs, follow
22
+ the active tab unless pinned, and stack the layout when narrower than 720px. Firefox gets the
23
+ same page as a sidebar.
24
+ - UI refresh: new palette and spacing in light and dark, icon toolbar with labels that collapse
25
+ in narrow layouts, segmented view switcher, a summary strip (renders, avoidable, wasted time,
26
+ top offender, best fix, all clickable), clearer selection and banners.
27
+ - `selfDuration` is now the component's own render time; the previous value (the whole subtree)
28
+ moved to `treeDuration`. Children that bailed out contribute nothing, as in the Profiler.
29
+ - `memoized` on every report (`React.memo` / `PureComponent`). Reports of unmemoized components
30
+ with avoidable prop changes explain that `React.memo` is needed as well; the panel's Fixes view
31
+ lists it first.
32
+ - Bridge `info()` reports `source` (`page` or `extension`) and `injected`. The panel warns when the
33
+ page runs its own copy next to the injected one.
34
+ - Extension: per-origin "Let React DevTools create the hook" toggle (popup and Settings). With it
35
+ on, the injector waits one task so React DevTools' own script can install the global hook first.
36
+ - Elements sidebar: an element without a React component above it now says so instead of
37
+ "not running".
38
+ - jsdom tests for `background.js`, `popup.js` and `sidebar.js` (shared fake `chrome`).
39
+ - Panel source moved to TypeScript (`extension/src/panel.ts`); `npm run build` writes the
40
+ committed `extension/panel.js`, and CI fails when it is out of date.
41
+ - Tree and live stream are virtualized (only the rows in view exist in the DOM); the stream now
42
+ keeps every buffered report instead of the last 300. `panel.html?demo&flood=5000` loads
43
+ synthetic data for scale testing.
44
+ - Keyboard: `/` focuses search, `f` opens the Fix tab, `Esc` clears the page highlight. The
45
+ details tab is remembered per origin.
46
+ - `different` object props show the differing leaves (`filters.page: 1 → 2`) without expanding
47
+ the whole value; the kind label names the first differing path.
48
+ - Root-cause page: click a root cause in a commit to see every commit it started, the components
49
+ it re-rendered avoidably, and the fixes for them.
50
+
51
+ ## 0.2.0
52
+
53
+ Library
54
+
55
+ - Every report carries `commitId` (shared by the reports of one React commit) and, in dev
56
+ builds, `source` (`_debugSource` on React <= 18, parsed from `_debugStack` on React 19).
57
+ - DevTools bridge protocol 2: `hello` carries library version, protocol, React renderers and
58
+ options; `pull(since)` for polling panels; `info()`, `configure()`, `getOptions()`,
59
+ `highlight(instanceId)`, `flashAvoidable(on)` (in-page overlay), `inspect(node)`.
60
+ - `getRenderers()` / `isProductionReact()`: what react-dom registered on the DevTools hook.
61
+ `attach` wraps `hook.inject` so hooks created by react-refresh (Vite) are covered too.
62
+ - `serializeOptions` / `deserializeOptions`, `VERSION`.
63
+ - An IIFE build (`extension/vendor/rerender-lens.js`, global `RerenderLens`) for injection.
64
+
65
+ Extension
66
+
67
+ - Store-ready packaging: `npm run build:ext` writes Chrome, Edge and Firefox zips; the manifest
68
+ version follows `package.json`; CI uploads the zips; the release workflow publishes to the
69
+ Chrome Web Store when secrets are configured. Listing text and privacy policy in
70
+ `extension/store/`.
71
+ - Zero-config mode: enable an origin from the toolbar popup or the panel's Settings and tick
72
+ *Inject the library*; the extension loads rerender-lens into the page before React.
73
+ Optional host permissions for non-local hosts; local development hosts are on by default.
74
+ - Three-state status (no page / no library / connected with library and React versions),
75
+ protocol-mismatch and production-build warnings.
76
+ - Polling fallback through `inspectedWindow.eval` when no content script is present.
77
+ - New views: Offenders (sortable table), Commits (root causes, render cascade, contexts),
78
+ Fixes (ranked by avoidable re-renders removed, with snippets). Per-report Fix tab.
79
+ - Hover-to-highlight and flash of avoidable renders in the page; "open source" links into the
80
+ Sources panel; Copy as Markdown; JSON export/import.
81
+ - Settings drawer driving `configure()` live, persisted per origin; panel state (filters,
82
+ collapsed nodes, split width) persisted per origin.
83
+ - Elements-panel sidebar for `$0`; toolbar badge with the avoidable count per tab.
84
+ - Reports are batched per animation frame; malformed payloads are ignored.
85
+ - Playwright end-to-end suite (`npm run e2e`) running Chromium with the extension against the
86
+ example app (relay, badge, injection, panel).
87
+
3
88
  ## 0.1.0
4
89
 
5
90
  Initial release.
6
91
 
7
- - `init(React, options)` patches `createElement` and the state hooks; tracked components report every update.
92
+ - `init(options)` observes React commits through the DevTools global hook and reports every
93
+ update of a tracked component. No patching, no wrappers: Fast Refresh, memo comparators,
94
+ forwardRef, classes and the automatic JSX runtime all work unchanged.
8
95
  - Change classification: `deep-equal`, `function`, `element`, `different`, `added`, `removed`.
9
96
  - Trigger classification: `props`, `parent`, `state`, `hooks`, `mixed`; `avoidable` flag.
10
- - Supports function components, `React.memo` (custom comparators preserved), `forwardRef`, `memo(forwardRef())`, class and `PureComponent` components.
11
- - `useWhyRerender(name, values)` hook for tracking one component without patching.
97
+ - `parent` (nearest ancestor that rendered, and why), `owner`, `path`, `instanceId`,
98
+ `renderCount`, `selfDuration` on every report.
99
+ - Hook state (`useState`, `useReducer`, `useSyncExternalStore`) and `useContext` diffs.
100
+ - `useWhyRerender(name, values)` hook for tracking one component without `init`.
12
101
  - `createCollector()` with `assertNoAvoidable()` for tests; `combineNotifiers()`.
13
- - `createDevtoolsNotifier()` posting structured-clone-safe reports on `window` for a DevTools panel.
14
- - `rerender-lens/jsx-runtime` and `rerender-lens/jsx-dev-runtime` for the automatic JSX runtime.
15
- - StrictMode double-render is not reported.
102
+ - `createDevtoolsNotifier()` posting structured-clone-safe reports on `window`.
103
+ - `ignoreHotReload` and `maxReportsPerComponent` options.
104
+ - Example Vite app with three deliberate re-render bugs.
package/README.md CHANGED
@@ -1,16 +1,75 @@
1
1
  # rerender-lens
2
2
 
3
- Find avoidable React re-renders and learn exactly which prop, state or hook caused them.
4
- A modern alternative to `why-did-you-render`: TypeScript, structured reports you can assert on
5
- in tests, a hook for tracking a single component, and a bridge for a DevTools panel.
3
+ Find avoidable React re-renders and see exactly what caused them: which prop, which state,
4
+ which context, and which ancestor started the update.
6
5
 
6
+ It reads the fiber tree after every commit through the same global hook React DevTools uses.
7
+ Nothing in React is patched or wrapped, so it works with Fast Refresh, `React.memo` comparators,
8
+ `forwardRef`, class components, the automatic JSX runtime, and any bundler.
9
+
10
+ ```
11
+ ▸ [rerender-lens] <ProductRow> avoidable re-render: 1 equal by value, 1 new function
12
+ - caused by <ProductPage> re-rendering (its state changed).
13
+ - prop "style" is a new reference but deep-equal to the previous value: memoize the object with useMemo, or hoist it to module scope if it is constant.
14
+ - prop "onSelect" is a new function instance on every render: wrap it in useCallback (or hoist it out of the parent's render).
15
+ at App > ProductPage > ProductRow
7
16
  ```
8
- [rerender-lens] <ProductRow> avoidable re-render: 1 equal by value, 1 new function
9
- - prop "style" is a new reference but deep-equal to the previous value: memoize the object with useMemo, or hoist it to module scope if it is constant.
10
- - prop "onSelect" is a new function instance on every render: wrap it in useCallback (or hoist it out of the parent's render).
17
+
18
+ Two ways to use it: the **DevTools extension** (a "Re-renders" panel next to Elements and
19
+ Console, no app code needed) or the **npm package** (console output, test assertions, and the
20
+ bridge the extension reads).
21
+
22
+ ## Chrome extension
23
+
24
+ The extension adds a **Re-renders** panel to DevTools: a component tree with avoidable counts,
25
+ why each component rendered, which ancestor started the cascade, the props and hooks that
26
+ changed, and the fix as a snippet you can copy. Other views rank components by wasted renders
27
+ (Offenders), group renders by React commit with their root cause (Commits), and rank every fix
28
+ by how many re-renders it removes (Fixes). It also gives you an Elements-panel sidebar for the
29
+ selected node, a badge with the avoidable count of the tab, hover-to-highlight in the page, and
30
+ "open source" links into the Sources panel.
31
+
32
+ ### Install
33
+
34
+ Until the Web Store listing is live, install it unpacked:
35
+
36
+ 1. Download `rerender-lens-chrome-<version>.zip` from the
37
+ [latest release](https://github.com/NexaLeaf/rerender-lens/releases) and unzip it, or build it
38
+ from a clone with `npm install && npm run build` and use the `extension/` folder.
39
+ 2. Open `chrome://extensions`, turn on **Developer mode**, click **Load unpacked**, pick the folder.
40
+ 3. Open your app, open DevTools, pick the **Re-renders** tab.
41
+
42
+ Edge loads the same folder from `edge://extensions`. Firefox 128+ uses the `firefox` zip from the
43
+ release, via `about:debugging`.
44
+
45
+ ### Connect a page
46
+
47
+ Local development hosts (`localhost`, `127.0.0.1`, `*.localhost`, `*.local`) work out of the box.
48
+ Any other site: click the toolbar icon and **Enable on this site** (a one-time host permission
49
+ for that origin only).
50
+
51
+ Then pick one of two modes:
52
+
53
+ - **Inject the library** (no app code): tick *Inject the library* in the toolbar popup or in the
54
+ panel's Settings and reload. The extension loads rerender-lens into the page before React,
55
+ tracking every `React.memo` / `PureComponent` by default. Change what is tracked from
56
+ Settings; the choice is saved per origin.
57
+ - **The page runs the library**: install the package and pass the DevTools notifier (see below).
58
+ This is the way to go when you also want console output or want to commit the setup.
59
+
60
+ ```ts
61
+ import { init, createDevtoolsNotifier } from 'rerender-lens';
62
+ init({ trackAllMemoized: true, silent: true, notifier: createDevtoolsNotifier() });
11
63
  ```
12
64
 
13
- ## Install
65
+ Production React builds are detected and flagged (names may be minified, hooks unlabeled). If
66
+ React DevTools is also installed and its Components tab comes up empty with injection on, tick
67
+ *Let React DevTools create the hook* for that origin.
68
+
69
+ `extension/README.md` has the details: keyboard shortcuts, how the transport works, Firefox and
70
+ Edge packaging, and the store listing.
71
+
72
+ ## Install the package
14
73
 
15
74
  ```sh
16
75
  npm i -D rerender-lens
@@ -20,16 +79,12 @@ Peer dependency: `react >= 16.8`. Tested with React 18 and 19.
20
79
 
21
80
  ## Setup
22
81
 
23
- Create a file that runs **before anything else imports React components**, and import it first
24
- in your entry point. Development only.
25
-
26
82
  ```ts
27
83
  // src/rerender-lens.ts
28
- import React from 'react'; // default import, not `import * as React`
29
84
  import { init } from 'rerender-lens';
30
85
 
31
86
  if (import.meta.env.DEV) {
32
- init(React, { trackAllMemoized: true });
87
+ init({ trackAllMemoized: true });
33
88
  }
34
89
  ```
35
90
 
@@ -40,31 +95,16 @@ import { createRoot } from 'react-dom/client';
40
95
  ...
41
96
  ```
42
97
 
43
- ### Automatic JSX runtime (Vite, Next, TS `react-jsx`)
44
-
45
- With the automatic runtime the compiler never calls `React.createElement`, so also point
46
- `jsxImportSource` at this package in development:
47
-
48
- ```ts
49
- // vite.config.ts
50
- export default defineConfig(({ mode }) => ({
51
- esbuild: mode === 'development' ? { jsxImportSource: 'rerender-lens' } : undefined,
52
- plugins: [react({ jsxImportSource: mode === 'development' ? 'rerender-lens' : 'react' })],
53
- }));
54
- ```
55
-
56
- ```jsonc
57
- // tsconfig.json (or a tsconfig.dev.json)
58
- { "compilerOptions": { "jsx": "react-jsx", "jsxImportSource": "rerender-lens" } }
59
- ```
98
+ Import the setup file before `react-dom`. React DOM looks for the DevTools hook once, when its
99
+ module loads; `init` creates the hook if nothing else did. If the React DevTools extension is
100
+ installed, or Vite's Fast Refresh preamble runs, the hook already exists and order does not matter.
60
101
 
61
- Anything still using `React.createElement` (the classic runtime, `React.cloneElement` of a new
62
- element, libraries) is covered by `init` alone.
102
+ No `jsxImportSource`, no default-import requirement, no Babel plugin.
63
103
 
64
104
  ## Choosing what to track
65
105
 
66
106
  ```ts
67
- init(React, {
107
+ init({
68
108
  trackAllMemoized: true, // every React.memo and PureComponent
69
109
  include: [/^Grid/, 'Sidebar'], // by display name: string, RegExp or predicate
70
110
  exclude: ['DevOverlay'],
@@ -81,7 +121,8 @@ export default track(memo(Sidebar));
81
121
  export const Cell = track((props: CellProps) => ..., 'Cell'); // name for anonymous arrows
82
122
  ```
83
123
 
84
- Marking sets the static `rerenderLens = true`; you can also set it by hand.
124
+ Marking sets the static `rerenderLens = true`; you can also set it by hand. `configure()`
125
+ changes any option at runtime without remounting anything.
85
126
 
86
127
  ## What a report contains
87
128
 
@@ -94,7 +135,16 @@ Every update of a tracked component produces a `RenderReport`:
94
135
  | `avoidable` | `true` when nothing genuinely changed |
95
136
  | `propChanges` | one entry per changed prop with `path`, `kind`, `prev`, `next` |
96
137
  | `stateChanges` | class components: `this.state` diff |
97
- | `hookChanges` | `useState`, `useReducer`, `useContext`, `useSyncExternalStore` values that changed |
138
+ | `hookChanges` | `useState`, `useReducer`, `useSyncExternalStore` and `useContext` values that changed; context entries carry `provider` (who renders it) and `changedKeys` / `totalKeys` for object values |
139
+ | `parent` | nearest ancestor that rendered in the same commit, and why |
140
+ | `owner` | component that created the element (dev builds) |
141
+ | `path` | component ancestry from the root |
142
+ | `instanceId`, `renderCount` | stable per mounted instance |
143
+ | `memoized` | `React.memo` / `PureComponent`: props alone decide whether it re-renders |
144
+ | `selfDuration`, `treeDuration` | own render time, and with everything below that rendered (dev/profiling builds) |
145
+ | `commitId` | shared by every report of one React commit |
146
+ | `commitPriority` | `immediate` (clicks, keys), `user-blocking` (continuous input), `normal` (transitions, async), `low`, `idle` |
147
+ | `source` | file, line and column where the element was created (dev builds) |
98
148
  | `reasons` | human-readable explanations with the fix |
99
149
 
100
150
  Change kinds:
@@ -107,37 +157,36 @@ Change kinds:
107
157
  | `different` | a real change | none needed |
108
158
  | `added` / `removed` | key appeared or disappeared | usually a real change |
109
159
 
110
- A `parent` trigger with no changes at all means: identical props, the parent re-rendered.
160
+ A `parent` trigger with no changes at all means: identical props, an ancestor re-rendered.
111
161
  Wrap the component in `React.memo`.
112
162
 
113
- By default only avoidable re-renders are printed to the console; pass `logAll: true` to print
114
- every report. The `notifier` always receives every report.
163
+ By default only avoidable re-renders are printed; pass `logAll: true` to print every report.
164
+ The `notifier` always receives every report.
115
165
 
116
166
  ## Use in tests
117
167
 
118
168
  ```ts
119
- import React from 'react';
120
169
  import { init, disable, createCollector } from 'rerender-lens';
121
170
 
122
171
  const collector = createCollector();
123
- beforeAll(() => init(React, { trackAllMemoized: true, silent: true, notifier: collector.notifier }));
172
+ beforeAll(() => init({ trackAllMemoized: true, silent: true, notifier: collector.notifier }));
124
173
  afterAll(disable);
125
174
  beforeEach(collector.clear);
126
175
 
127
176
  test('typing in the search box does not re-render the grid rows', async () => {
128
177
  render(<ProductPage />);
129
178
  await user.type(screen.getByRole('searchbox'), 'abc');
130
- collector.assertNoAvoidable(); // throws with every component and reason
179
+ collector.assertNoAvoidable(); // throws listing every component and reason
131
180
  // or: expect(collector.avoidable).toHaveLength(0)
132
181
  });
133
182
  ```
134
183
 
135
- Import order matters here too: `init` must run before the modules under test read
136
- `React.useState` etc. A `setupFiles` entry in Vitest/Jest is the usual place.
184
+ In Vitest or Jest, call `ensureDevtoolsHook()` (or `init`) from a `setupFiles` entry so the hook
185
+ exists before `react-dom` is imported by your tests.
137
186
 
138
187
  ## Track a single component from the inside
139
188
 
140
- No patching, works anywhere, including inside library packages or Storybook:
189
+ Works anywhere without `init`, for example inside library packages or Storybook:
141
190
 
142
191
  ```ts
143
192
  import { useWhyRerender } from 'rerender-lens';
@@ -149,35 +198,52 @@ function Row(props: RowProps) {
149
198
  }
150
199
  ```
151
200
 
152
- Pass whatever values you want compared. Reports use the notifier from `init` when it was
153
- called, or the `options` third argument.
201
+ Pass whatever values you want compared.
154
202
 
155
203
  ## DevTools bridge
156
204
 
157
205
  ```ts
158
206
  import { createDevtoolsNotifier } from 'rerender-lens';
159
207
 
160
- init(React, { trackAllMemoized: true, silent: true, notifier: createDevtoolsNotifier() });
208
+ init({ trackAllMemoized: true, silent: true, notifier: createDevtoolsNotifier() });
161
209
  ```
162
210
 
163
211
  Each report is serialized (functions become `ƒ name`, elements `<Type>`, cycles cut) and posted
164
- on `window` as `{ __rerenderLens: true, version: 1, type: 'report', payload }`. The last 300
165
- reports are buffered; `window.__RERENDER_LENS_DEVTOOLS__.replay()` re-posts them and `.clear()`
166
- drops them. Any extension or in-page panel can consume this.
212
+ on `window` as `{ __rerenderLens: true, version: 2, type: 'report', payload }`. The last 300
213
+ reports are buffered. The bridge at `window.__RERENDER_LENS_DEVTOOLS__` exposes:
214
+
215
+ | Method | |
216
+ | --- | --- |
217
+ | `replay()` / `clear()` | re-post or drop the buffer |
218
+ | `pull(since)` | reports newer than a sequence number, for panels that poll instead of listening |
219
+ | `info()` | library version, protocol, React renderers (version, dev/prod), current options |
220
+ | `configure(options)` / `getOptions()` | change options at runtime; matchers as strings (`"/^Grid/"`) |
221
+ | `highlight(instanceId)` / `flashAvoidable(on)` | outline a component's DOM in the page, or flash avoidable renders |
222
+ | `inspect(node)` | component, instance id and recent reports for a DOM node (DevTools `$0`) |
223
+
224
+ The DevTools extension consumes this (see [Chrome extension](#chrome-extension) above); with
225
+ injection enabled it needs no `init` call at all.
226
+
227
+ Every report also carries `commitId` (shared by all reports of one React commit) and, in dev
228
+ builds, `source` (where the element was created).
167
229
 
168
230
  ## API
169
231
 
170
232
  ```ts
171
- init(React, options?): () => void // returns disable
172
- configure(options): void // merge options at runtime
173
- disable(): void // restore React
233
+ init(options?): () => void // returns disable
234
+ configure(options): void // merge options at runtime
235
+ disable(): void
174
236
  isEnabled(): boolean
175
237
  track(component, name?): component
238
+ ensureDevtoolsHook(): hook // create the global hook early (test setup files)
176
239
  useWhyRerender(name, values, options?)
177
240
  createCollector(): { reports, avoidable, notifier, clear, assertNoAvoidable }
178
241
  combineNotifiers(...notifiers): Notifier
179
- createDevtoolsNotifier({ bufferSize?, target?, maxDepth? }): Notifier
180
- deepEqual(a, b), diffRecords(prev, next), classify(prev, next) // the primitives
242
+ createDevtoolsNotifier({ bufferSize?, target?, maxDepth?, flashAvoidable? }): Notifier
243
+ getRenderers(), isProductionReact() // what react-dom registered on the DevTools hook
244
+ serializeOptions(o), deserializeOptions(o) // Options <-> JSON-safe form used by the bridge
245
+ VERSION
246
+ deepEqual(a, b), diffRecords(prev, next), classify(prev, next)
181
247
  ```
182
248
 
183
249
  `Options`:
@@ -187,45 +253,61 @@ deepEqual(a, b), diffRecords(prev, next), classify(prev, next) // the primitiv
187
253
  | `trackAllMemoized` | `false` | track every `memo` / `PureComponent` |
188
254
  | `trackAllComponents` | `false` | track everything (noisy) |
189
255
  | `include` / `exclude` | | display-name matchers |
190
- | `trackHooks` | `true` | capture and diff hook values |
256
+ | `trackHooks` | `true` | diff hook state and contexts |
191
257
  | `logAll` | `false` | print non-avoidable reports too |
192
258
  | `silent` | `false` | never print; notifier still runs |
193
259
  | `notifier` | | receives every `RenderReport` |
194
260
  | `collapse` | `true` | `console.groupCollapsed` vs `console.group` |
195
261
  | `console` | `console` | sink for printing |
262
+ | `ignoreHotReload` | `true` | skip commits caused by Fast Refresh |
263
+ | `maxReportsPerComponent` | `0` | stop printing a component after N reports |
196
264
 
197
265
  ## How it works, and the caveats that follow
198
266
 
199
- - `init` replaces `React.createElement` with one that maps a tracked component type to a cached
200
- wrapper. The wrapper renders the original, keeps the previous props and hook values in a ref,
201
- and diffs them on the next render. Class components get a subclass whose `componentDidUpdate`
202
- diffs `props` and `state` and then calls yours.
203
- - Hook values are captured by patching `React.useState`, `useReducer`, `useContext` and
204
- `useSyncExternalStore`. This only reaches code that reads those functions **after** `init`
205
- ran, which is why the setup file must be imported first. With `import * as React`, the
206
- namespace is read-only in most bundlers; pass the default import.
207
- - A wrapper is a different element type from the original, so the first time a component
208
- becomes tracked (or stops being tracked after `configure`) it remounts. Decide tracking at
209
- startup; use `configure` for reporting options.
210
- - The mount render is never reported. Under `StrictMode` the duplicate dev render is detected
211
- (same props object, same hook values) and skipped.
212
- - `trackHooks: false` disables state-triggered reports for function components, because the
213
- wrapper then cannot distinguish a state update from a StrictMode re-invocation.
214
- - Diffing is structural and happens during render. It is meant for development; do not enable
215
- it in production builds.
267
+ - After each commit, the fiber tree is walked from the root, skipping subtrees React bailed out
268
+ of. A component counts as re-rendered when React set its `PerformedWork` flag. Props, class
269
+ state, hook state nodes and context dependencies are compared against the fiber's alternate.
270
+ - Hook labels come from React's dev-only hook type list when it maps one-to-one onto the state
271
+ nodes; otherwise nodes are labelled by inspection (`useState`, `useReducer`,
272
+ `useSyncExternalStore`, or `state` for internal nodes of `useTransition` and friends).
273
+ - A commit in which Fast Refresh swapped a component's code is skipped entirely
274
+ (`ignoreHotReload`). The edited component's children re-render with identical props during
275
+ that commit, which would otherwise look like avoidable re-renders.
276
+ - The mount render is never reported. StrictMode's double render happens inside one commit and
277
+ is reported once.
278
+ - Everything runs during React's commit callback, synchronously. It is meant for development;
279
+ keep it out of production builds.
280
+ - Fiber field names have been stable since React 16.9 (`flags` was `effectTag` before 17; both
281
+ are handled). Future React versions may change internals; the walk is wrapped so a failure
282
+ logs one warning instead of breaking the app.
283
+
284
+ ## Example app
285
+
286
+ `examples/vite-react` has three deliberate bugs. From the repo root:
287
+
288
+ ```sh
289
+ npm install
290
+ npm --prefix examples/vite-react install
291
+ npm run dev:example
292
+ ```
293
+
294
+ `http://localhost:5199/` runs the library itself and reports to the console and the extension.
295
+ `http://localhost:5199/plain.html` is the same app without the library, for trying the
296
+ extension's *Inject the library* mode.
216
297
 
217
298
  ## Migrating from why-did-you-render
218
299
 
219
300
  | why-did-you-render | rerender-lens |
220
301
  | --- | --- |
221
- | `whyDidYouRender(React, opts)` | `init(React, opts)` |
302
+ | `whyDidYouRender(React, opts)` | `init(opts)` |
222
303
  | `trackAllPureComponents` | `trackAllMemoized` |
223
304
  | `Comp.whyDidYouRender = true` | `track(Comp)` or `Comp.rerenderLens = true` |
224
305
  | `include` / `exclude` (RegExp[]) | same, plus strings and predicates |
225
306
  | `trackHooks` | same |
226
307
  | `logOnDifferentValues` | `logAll` |
308
+ | `logOwnerReasons` | always on: `parent` and `owner` fields |
227
309
  | `notifier` (`{ Component, prevProps, ... }`) | `notifier` (`RenderReport`) |
228
- | `jsxImportSource: '@welldone-software/why-did-you-render'` | `jsxImportSource: 'rerender-lens'` |
310
+ | `jsxImportSource: '@welldone-software/why-did-you-render'` | not needed |
229
311
 
230
312
  ## License
231
313