react-perf-recorder 0.1.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/LICENSE +21 -0
- package/README.md +65 -0
- package/claude/README.md +10 -0
- package/claude/agents/perf-recorder.md +44 -0
- package/claude/mcp.json +8 -0
- package/claude/skills/react-perf-recorder/SKILL.md +52 -0
- package/claude/skills/react-perf-recorder/references/causes-and-actions.md +47 -0
- package/claude/skills/react-perf-recorder/references/from-scripts.md +36 -0
- package/claude/skills/react-perf-recorder/references/getting-a-recording.md +41 -0
- package/claude/skills/react-perf-recorder/references/measuring-a-fix.md +42 -0
- package/claude/skills/react-perf-recorder/references/panel.md +21 -0
- package/claude/skills/react-perf-recorder/references/reading-a-recording.md +55 -0
- package/dist/browser/chunk-7DJUCCWG.js +447 -0
- package/dist/browser/chunk-NTY2W4HE.js +182 -0
- package/dist/browser/client.d.ts +589 -0
- package/dist/browser/client.js +7161 -0
- package/dist/browser/index-BlkKhwHe.d.ts +585 -0
- package/dist/browser/plugins/proxy-memoize.d.ts +7 -0
- package/dist/browser/plugins/proxy-memoize.js +41 -0
- package/dist/browser/plugins/react-query.d.ts +5 -0
- package/dist/browser/plugins/react-query.js +74 -0
- package/dist/browser/plugins/zustand.d.ts +11 -0
- package/dist/browser/plugins/zustand.js +171 -0
- package/dist/browser/runtime.d.ts +1 -0
- package/dist/browser/runtime.js +12 -0
- package/dist/cli.js +23119 -0
- package/dist/engine.iife.js +3661 -0
- package/dist/node/chunk-HS2BJBJX.js +170 -0
- package/dist/node/plugin-api-zXFxjYba.d.cts +61 -0
- package/dist/node/plugin-api-zXFxjYba.d.ts +61 -0
- package/dist/node/plugins/proxy-memoize.cjs +214 -0
- package/dist/node/plugins/proxy-memoize.d.cts +16 -0
- package/dist/node/plugins/proxy-memoize.d.ts +16 -0
- package/dist/node/plugins/proxy-memoize.js +51 -0
- package/dist/node/plugins/react-query.cjs +32 -0
- package/dist/node/plugins/react-query.d.cts +6 -0
- package/dist/node/plugins/react-query.d.ts +6 -0
- package/dist/node/plugins/react-query.js +7 -0
- package/dist/node/plugins/zustand.cjs +247 -0
- package/dist/node/plugins/zustand.d.cts +17 -0
- package/dist/node/plugins/zustand.d.ts +17 -0
- package/dist/node/plugins/zustand.js +61 -0
- package/dist/node/vite.cjs +1262 -0
- package/dist/node/vite.d.cts +103 -0
- package/dist/node/vite.d.ts +103 -0
- package/dist/node/vite.js +1072 -0
- package/docs/contributing.md +24 -0
- package/docs/how-it-works.md +34 -0
- package/docs/mcp.md +62 -0
- package/docs/measuring-a-fix.md +65 -0
- package/docs/options.md +22 -0
- package/docs/panel.md +59 -0
- package/docs/plugins.md +57 -0
- package/docs/recording.md +44 -0
- package/package.json +139 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Eugene
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
# react-perf-recorder
|
|
2
|
+
|
|
3
|
+
Record why a React app re-renders, from the page itself: press **Rec**, use the app, press **Stop**. The report
|
|
4
|
+
names the component that started each render cascade and why — the hook and its line of code, the store action and
|
|
5
|
+
the keys it changed, the props that broke `memo` — and an agent reads the same recordings through an MCP server.
|
|
6
|
+
|
|
7
|
+
It runs only in the Vite dev server and never ships to a build. React 18.2+ and 19.1+.
|
|
8
|
+
|
|
9
|
+
## Install
|
|
10
|
+
|
|
11
|
+
```sh
|
|
12
|
+
npm i -D -E react-perf-recorder
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
```ts
|
|
16
|
+
// vite.config.ts
|
|
17
|
+
import { perfRecorder } from 'react-perf-recorder/vite';
|
|
18
|
+
import { zustand } from 'react-perf-recorder/plugins/zustand';
|
|
19
|
+
import { proxyMemoize } from 'react-perf-recorder/plugins/proxy-memoize';
|
|
20
|
+
import { reactQuery } from 'react-perf-recorder/plugins/react-query';
|
|
21
|
+
|
|
22
|
+
export default defineConfig({
|
|
23
|
+
plugins: [react(), perfRecorder({ plugins: [zustand(), proxyMemoize(), reactQuery()] })],
|
|
24
|
+
});
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
Open the dev page: the panel sits in a corner. **Alt+Shift+R** starts and stops a recording, **Alt+Shift+S** picks
|
|
28
|
+
an area of the page to record alone.
|
|
29
|
+
|
|
30
|
+
## What you get
|
|
31
|
+
|
|
32
|
+
- **The root to fix** — the component that started a cascade, with its reason (`state now`,
|
|
33
|
+
`store chat selectMessages SAME-CONTENT`, `context Theme`) and the hook chain down to the line.
|
|
34
|
+
- **The way a render came down** — from the cause through each parent to the component, with the props each one
|
|
35
|
+
handed on; the link with equal props is where a `memo` stops the rest.
|
|
36
|
+
- **Wasted renders** — those after which nothing in the DOM changed, and remounts.
|
|
37
|
+
- **Causes** — the store action, query, timer, socket message or click behind each commit.
|
|
38
|
+
- **A timeline** of actions and commits; a picked commit shows its cascade as a tree and outlines its components on
|
|
39
|
+
the page.
|
|
40
|
+
- **Memos that miss** — a `useMemo` that recomputes on every render, and the dependency that moved.
|
|
41
|
+
- **Before → after** — `↻ Repeat` reloads the page and does the same actions again, so a fix is measured.
|
|
42
|
+
|
|
43
|
+
## With an assistant
|
|
44
|
+
|
|
45
|
+
```sh
|
|
46
|
+
npx react-perf-recorder init-claude
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
Adds a skill, an agent and the MCP server to the project. The agent records a scenario — or reads the one you
|
|
50
|
+
recorded — and answers with the cascade root, the hook behind it and the file to change.
|
|
51
|
+
|
|
52
|
+
## Docs
|
|
53
|
+
|
|
54
|
+
- [The panel and the report](docs/panel.md)
|
|
55
|
+
- [What a recording holds](docs/recording.md)
|
|
56
|
+
- [Measuring a fix](docs/measuring-a-fix.md)
|
|
57
|
+
- [Options](docs/options.md)
|
|
58
|
+
- [MCP server, CLI and scripts](docs/mcp.md)
|
|
59
|
+
- [Plugins](docs/plugins.md)
|
|
60
|
+
- [How it works, and its limits](docs/how-it-works.md)
|
|
61
|
+
- [Working on it](docs/contributing.md)
|
|
62
|
+
|
|
63
|
+
## License
|
|
64
|
+
|
|
65
|
+
MIT
|
package/claude/README.md
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
# Claude Code files
|
|
2
|
+
|
|
3
|
+
What is here is copied into a project by `npx react-perf-recorder init-claude`:
|
|
4
|
+
|
|
5
|
+
- `skills/react-perf-recorder/` — how to record and how to read a recording;
|
|
6
|
+
- `agents/perf-recorder.md` — an agent that records a scenario and answers with the cause and the numbers;
|
|
7
|
+
- `mcp.json` — the MCP server entry, merged into the project's `.mcp.json`.
|
|
8
|
+
|
|
9
|
+
The package's own `.claude/` links to these files, so working on the recorder uses the same skill everyone else
|
|
10
|
+
gets.
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: perf-recorder
|
|
3
|
+
description: Finds why a React page re-renders and proves a fix with numbers. Records a scenario with react-perf-recorder — from the page load, from a scenario it drives, or from one the person recorded — and answers with the cascade root, the hook or store behind it, and the file and line to change. Use when a page feels slow, flashes on its own, or a change has to be shown to have helped.
|
|
4
|
+
tools: mcp__react-perf-recorder, mcp__playwright, Read, Grep, Glob, Bash
|
|
5
|
+
skills:
|
|
6
|
+
- react-perf-recorder
|
|
7
|
+
model: sonnet
|
|
8
|
+
---
|
|
9
|
+
|
|
10
|
+
# Finding a re-render
|
|
11
|
+
|
|
12
|
+
Work by the `react-perf-recorder` skill; open its references when you get to them.
|
|
13
|
+
|
|
14
|
+
1. **Conditions from the caller**: the URL, what the page should do, whether they will record it themselves.
|
|
15
|
+
Nothing to reproduce means nothing to measure — ask. Pointed at a component ("this list", a file), read the file
|
|
16
|
+
and take the name it is exported under: that is what `scope` and `watch` take.
|
|
17
|
+
2. **The recorder is on the page** — `window.__REACT_PERF_RECORDER__` or the panel in a corner. Not there in a
|
|
18
|
+
production build, and that is the answer.
|
|
19
|
+
3. **Record** — the person's own recording (`wait_for_recording`) first; else `record_page`, one scenario, 3–12
|
|
20
|
+
seconds.
|
|
21
|
+
4. **Read** — one `get_recording` summary; follow the hook chain to the app's own code, left of `[package]`. Open
|
|
22
|
+
another section only for what the summary left open.
|
|
23
|
+
5. **Answer at the confidence the recording gives.** When it names the root, the reason and the `file:line`, that is
|
|
24
|
+
the answer — stop. Measure a fix only when the cause is a guess or numbers were asked for, by
|
|
25
|
+
`references/measuring-a-fix.md` — in a git worktree, so the person's working tree stays as it was.
|
|
26
|
+
|
|
27
|
+
## Boundaries
|
|
28
|
+
|
|
29
|
+
- About ten tool calls is the job; past fifteen, answer with what you have and what is left unchecked.
|
|
30
|
+
- Read sections, not `recording.json`.
|
|
31
|
+
- Do not commit, stage or switch branches.
|
|
32
|
+
- Numbers go into the answer, never into a file of the repository.
|
|
33
|
+
- Close the browser you opened, even when the run failed. Scratch files go to `.agent-artifacts/`, and you delete
|
|
34
|
+
yours.
|
|
35
|
+
- One recording at a time: if the engine is busy, someone else is recording — say so.
|
|
36
|
+
|
|
37
|
+
## What to answer
|
|
38
|
+
|
|
39
|
+
- **The cause**: the cascade root, the reason with the hook or selector, `file:line`.
|
|
40
|
+
- **The fix**: what to change and where; before and after numbers, or "not measured" and why.
|
|
41
|
+
- **The conditions**: page, viewport, throttling, the data, how long it ran.
|
|
42
|
+
- **What you did not check**, as a list.
|
|
43
|
+
|
|
44
|
+
No retelling of the steps; thirty lines is plenty.
|
package/claude/mcp.json
ADDED
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: react-perf-recorder
|
|
3
|
+
description: Record why a React app re-renders and read the recording — cascade roots, the hook or store behind each one, what scheduled the commit, renders that changed nothing. Use when a page feels slow, flashes on its own, or a fix has to be proved with before-and-after numbers.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# react-perf-recorder
|
|
7
|
+
|
|
8
|
+
A dev-only Vite plugin that records React re-renders from the page. Recordings land in a folder
|
|
9
|
+
(`.agent-artifacts/perf-recorder`, or `REACT_PERF_RECORDER_DIR`) and this session reads them through the
|
|
10
|
+
`react-perf-recorder` MCP server.
|
|
11
|
+
|
|
12
|
+
**Never write measured numbers into a file of the repository.** They are true for one machine and one moment;
|
|
13
|
+
numbers belong in the answer.
|
|
14
|
+
|
|
15
|
+
## Getting a recording
|
|
16
|
+
|
|
17
|
+
| The situation | What to do |
|
|
18
|
+
| ---------------------------------------------------- | ------------------------------------------------------------------------------------------------------- |
|
|
19
|
+
| The person reproduces the problem in their browser | Ask them to press **Rec**, do it, press **Stop**; meanwhile `wait_for_recording`, then `get_recording`. |
|
|
20
|
+
| You drive the page | `record_page` — one call, one recording; clicks and typing go in a `script` module. |
|
|
21
|
+
| The problem is the page load | `record_page` with `fromLoad`, or the panel's `↺ Page load`. |
|
|
22
|
+
| Before and after a fix, nothing waits on the network | `record_page` with `replay: <id>` after the change → `compare_recordings`. |
|
|
23
|
+
| Before and after a fix, actions wait on requests | Two `record_page` runs with the same `script`; compare them yourself. |
|
|
24
|
+
|
|
25
|
+
About one component: pass the name it is exported under as `scope`. Details, sign-in and the traps of each route:
|
|
26
|
+
`references/getting-a-recording.md`.
|
|
27
|
+
|
|
28
|
+
## Reading it
|
|
29
|
+
|
|
30
|
+
**One call usually holds the answer.** The default `summary` of `get_recording` has the totals, the cascade roots
|
|
31
|
+
with their reason, hook chain and `file:line`, what scheduled the commits, the costliest actions, the memos that
|
|
32
|
+
keep recomputing and the plugins' highlights. Read the file it points at; open another section only for what the
|
|
33
|
+
summary leaves open — `components` for the way a render came down, `timeline` for one commit's cascade.
|
|
34
|
+
|
|
35
|
+
No `react-perf-recorder` tools in this session? The server starts with the session; from a terminal the same data
|
|
36
|
+
is `node node_modules/react-perf-recorder/dist/cli.js list`, `show latest --section roots`, and `pull` to wait for
|
|
37
|
+
the next finished recording.
|
|
38
|
+
|
|
39
|
+
## References
|
|
40
|
+
|
|
41
|
+
- `references/getting-a-recording.md` — `record_page`, the area, sign-in, fast recordings.
|
|
42
|
+
- `references/measuring-a-fix.md` — before and after: replay or script, a worktree for the change, reading the result.
|
|
43
|
+
- `references/reading-a-recording.md` — roots, reasons, hook chains, components and their ways, memos.
|
|
44
|
+
- `references/causes-and-actions.md` — what scheduled each commit, the person's actions, plugin sections, traps.
|
|
45
|
+
- `references/panel.md` — the panel, for guiding a person who records it themselves.
|
|
46
|
+
- `references/from-scripts.md` — the page API for a Playwright or CDP script.
|
|
47
|
+
|
|
48
|
+
## Before you finish
|
|
49
|
+
|
|
50
|
+
- The cause is not a guess: name the root, the reason and the `file:line` the recording gave you.
|
|
51
|
+
- "This component does not re-render" is a claim only after `watch` or `components` says so.
|
|
52
|
+
- Close the browser you opened, and say what you did not check.
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
# Causes, actions, plugins, traps
|
|
2
|
+
|
|
3
|
+
## Causes
|
|
4
|
+
|
|
5
|
+
What scheduled each commit, aimed at the components it actually updated:
|
|
6
|
+
|
|
7
|
+
| Cause | Means |
|
|
8
|
+
| ------------------------------------- | -------------------------------------------------------------------------------- |
|
|
9
|
+
| `zustand:<action>` | a store write, with the keys it changed and a `SAME-CONTENT` mark |
|
|
10
|
+
| `react-query:fetch → success <key>` | a query's events, one cause per query and commit, on its subscribers' commit |
|
|
11
|
+
| `core:input <event>` | the person's click, keystroke, scroll |
|
|
12
|
+
| `core:message WebSocket` / `Worker` | a frame arrived |
|
|
13
|
+
| `core:timer setInterval <fn> @ src/…` | a timer, with the place it was started |
|
|
14
|
+
| `core:navigation push` | a navigation |
|
|
15
|
+
| `core:effect @ src/hooks/useX.ts` | a setState from an effect |
|
|
16
|
+
| `core:update <fn> @ src/…` | a plain call in the app's code; `(<package>)` when it came from inside a library |
|
|
17
|
+
| `core:none` | React scheduled the work itself — rare, and worth a second look |
|
|
18
|
+
|
|
19
|
+
A cause goes to the roots whose components it updated: a subscriber of `priceStore` gets
|
|
20
|
+
`zustand:priceStore.setState`, not the action that landed in the same commit.
|
|
21
|
+
|
|
22
|
+
## Actions
|
|
23
|
+
|
|
24
|
+
`section: actions` cuts the recording into action → consequences: each action runs until the next one or a second
|
|
25
|
+
of quiet. `reaction` is the commits during the person's event, `background` everything else (ticks, polling,
|
|
26
|
+
deferred work). Typing gets renders per character, a click the input latency. Typed values are never recorded, only
|
|
27
|
+
their length.
|
|
28
|
+
|
|
29
|
+
## Plugin sections
|
|
30
|
+
|
|
31
|
+
- `plugin:proxy-memoize` — calls and recomputes per selector; `evicting` means a `memoizeWithArgs` cache keeps
|
|
32
|
+
pushing out answers still in use. The fix is a memoized selector per row, not a bigger `size`. An unnamed selector
|
|
33
|
+
goes by where it was created: `memoize in Row · Messages.tsx`.
|
|
34
|
+
- `plugin:zustand` — the stores it saw and the updates they made.
|
|
35
|
+
- `plugin:react-query` — query events by kind and key.
|
|
36
|
+
|
|
37
|
+
A plugin whose library is not on the page has `active: false` and is left out of the summary.
|
|
38
|
+
|
|
39
|
+
## Traps
|
|
40
|
+
|
|
41
|
+
- **HMR during a recording** makes wide react-refresh commits; it is noted in `warnings`, and the run is better
|
|
42
|
+
repeated.
|
|
43
|
+
- **Store writes that led to no commit are not in the recording** — causes are attached to commits.
|
|
44
|
+
- **Two timers that updated the same component between commits** are attributed to the first.
|
|
45
|
+
- **One recording at a time**, panel or script: the second start gets an error naming the owner.
|
|
46
|
+
- **StrictMode** doubles render-time selector calls.
|
|
47
|
+
- **Highlights cost frame time**: a recording made with them on says so in `warnings`.
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
# Driving the recorder from a script
|
|
2
|
+
|
|
3
|
+
The page carries the engine at `window.__REACT_PERF_RECORDER__.engine`, so a Playwright or CDP script can record
|
|
4
|
+
without touching the panel. `docs/mcp.md` in the repository has the full API; this is what a measuring run
|
|
5
|
+
needs.
|
|
6
|
+
|
|
7
|
+
```js
|
|
8
|
+
// Playwright: an `async (page) => {}` script
|
|
9
|
+
await page.goto('http://localhost:5173/some/page');
|
|
10
|
+
await page.getByTestId('ready').waitFor();
|
|
11
|
+
|
|
12
|
+
const recording = await page.evaluate(async () => {
|
|
13
|
+
const { engine } = window.__REACT_PERF_RECORDER__;
|
|
14
|
+
engine.start({ source: 'script', label: 'tab switch', highlight: false });
|
|
15
|
+
document.querySelector('[data-testid="tab-orders"]').click();
|
|
16
|
+
await new Promise((r) => setTimeout(r, 1500));
|
|
17
|
+
return engine.stop(); // the Recording, and it is saved to the sessions folder too
|
|
18
|
+
});
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
- `engine.record(ms, options)` is start, wait, stop in one call; `engine.last` keeps the last recording.
|
|
22
|
+
- Options: `scope` (an area — `{ names: ['OrdersPanel', 'PositionTable'] }` or `{ selector }`), `watch` (component
|
|
23
|
+
names to follow), `zones` (named parts of the page, by selector), `label`, `highlight`, `sampleReasons` (fast),
|
|
24
|
+
`frames`, `hookNames`,
|
|
25
|
+
`prune`, `actions`, `bigCommit`, `timeline` (how many commits to keep), `meta`.
|
|
26
|
+
- The answer carries `id`: read the whole thing later with `get_recording`, and compare two runs with
|
|
27
|
+
`compare_recordings`.
|
|
28
|
+
- No engine on the page means the Vite plugin is not there — say so instead of measuring something else.
|
|
29
|
+
|
|
30
|
+
**Conditions decide whether two runs can be compared.** Same viewport, same data, same CPU throttling, same
|
|
31
|
+
account. Throttle through CDP (`Emulation.setCPUThrottlingRate`) if the machine is too fast to show the problem,
|
|
32
|
+
and say in the answer what the conditions were.
|
|
33
|
+
|
|
34
|
+
**A click under CPU throttling can hang** a Playwright call: it waits for the element to stop moving, and a page
|
|
35
|
+
that re-renders constantly never does. Click from inside the page (`element.click()` in `evaluate`) or turn the
|
|
36
|
+
throttling off for the click.
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
# Getting a recording
|
|
2
|
+
|
|
3
|
+
## The person records
|
|
4
|
+
|
|
5
|
+
Ask them to press **Rec** (or **Alt+Shift+R**), do the thing, press **Stop**. Call `wait_for_recording` meanwhile
|
|
6
|
+
(two minutes by default), then `get_recording`. Their recording is worth more than one you stage: it is the thing
|
|
7
|
+
that annoyed them. `references/panel.md` has what to tell them about the panel.
|
|
8
|
+
|
|
9
|
+
## You record: `record_page`
|
|
10
|
+
|
|
11
|
+
It opens the page in a browser of its own, records and returns the session id. It needs the dev server up and
|
|
12
|
+
`playwright` in the project — not a dependency of this package; its absence is an answer, then ask for a recording
|
|
13
|
+
from the panel.
|
|
14
|
+
|
|
15
|
+
- `ms` — 3–12 seconds of one scenario; `fromLoad` records the page load from its first commit.
|
|
16
|
+
- `script` — a module with `export default async (page) => {…}`, run while recording: clicks, typing. Wait for what
|
|
17
|
+
shows the result (a list, a spinner gone), not for a time.
|
|
18
|
+
- `scope: 'MessageList'` — only what renders inside that component; a render from above is kept as an outside root
|
|
19
|
+
with its reason. Read the component's file and take the name it is exported under. An area not on the page
|
|
20
|
+
answers with the names that are.
|
|
21
|
+
- `watch: ['MessageList']` — the whole page recorded, that component's renders counted and attributed on top.
|
|
22
|
+
- `sample: true` — about twice as fast on lists of thousands: parent-caused reasons are a sample (`sampled` on a
|
|
23
|
+
component), counts are exact, no ways or commit cascades are kept.
|
|
24
|
+
- `viewport`, `throttle` (CPU slowdown) — keep them the same across runs that will be compared.
|
|
25
|
+
|
|
26
|
+
## Before and after
|
|
27
|
+
|
|
28
|
+
`references/measuring-a-fix.md`: replay or script, a worktree for the change, and how to read the comparison.
|
|
29
|
+
|
|
30
|
+
## Behind a sign-in
|
|
31
|
+
|
|
32
|
+
In this order:
|
|
33
|
+
|
|
34
|
+
1. `via` — a link that signs the browser in (a debug URL with a token, a magic link). It is opened first, not
|
|
35
|
+
recorded, and nothing about it is stored.
|
|
36
|
+
2. A session saved once by `react-perf-recorder login <url>` — a real browser for a real sign-in, or
|
|
37
|
+
`--for <selector>` / `--wait <ms>` when the link signs in by itself — used by default from then on.
|
|
38
|
+
3. `cdp` — a browser the person already has open and signed in. It is theirs and is never closed.
|
|
39
|
+
|
|
40
|
+
A page that redirects to a login says so instead of recording the login form; then ask the person to record from
|
|
41
|
+
the panel. A token never reaches a recording: every URL a session keeps is masked.
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
# Measuring a fix
|
|
2
|
+
|
|
3
|
+
Two recordings of one scenario — the code as it was, and the change — then `compare_recordings`. Do it when the cause
|
|
4
|
+
is a guess or numbers were asked for; a recording that names the root, reason and `file:line` is an answer already.
|
|
5
|
+
|
|
6
|
+
## Route
|
|
7
|
+
|
|
8
|
+
1. The recording with the problem — the person's, or `record_page`. Keep its id.
|
|
9
|
+
2. The change, in a git worktree when there is git (below); otherwise in place, and put back afterwards.
|
|
10
|
+
3. The same scenario on the change:
|
|
11
|
+
- actions that happen in the page — `record_page` with `replay: <id>`;
|
|
12
|
+
- actions that wait on requests — a `script` that waits for the data (the list, the spinner gone), run on both
|
|
13
|
+
sides; a replay does not wait for data.
|
|
14
|
+
4. `compare_recordings` with `before: <id>`, `after: <new id>`.
|
|
15
|
+
5. A change that is not kept leaves nothing behind: `git worktree remove`, or the file as it was.
|
|
16
|
+
|
|
17
|
+
## Worktree
|
|
18
|
+
|
|
19
|
+
The working tree may hold someone else's changes; do not edit it for a measurement.
|
|
20
|
+
|
|
21
|
+
```sh
|
|
22
|
+
git worktree add --detach ../<app>-fix && cd ../<app>-fix && npm ci
|
|
23
|
+
REACT_PERF_RECORDER_DIR=<abs path of the first checkout>/.agent-artifacts/perf-recorder npm run dev -- --port <other port>
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
- `--detach`: no branch is made, and none is switched to.
|
|
27
|
+
- The worktree is the last commit, without uncommitted changes. When `git status` shows changes to files the page
|
|
28
|
+
uses, record "before" in the worktree as well, before editing, so the sides differ by the fix alone.
|
|
29
|
+
- The variable puts both servers' sessions in the folder the MCP server reads; an `outDir` in the project's config
|
|
30
|
+
overrides it — then set the same absolute path there.
|
|
31
|
+
- `record_page` on the second port: `url` with that port, also for a replay (it takes the recording's URL).
|
|
32
|
+
- Stop that dev server and remove the worktree when done, even when the run failed.
|
|
33
|
+
|
|
34
|
+
## Fair, and read right
|
|
35
|
+
|
|
36
|
+
- Same viewport, throttling, data, account and area on both sides; `record_page` keeps outlines off.
|
|
37
|
+
- Compare renders, not milliseconds — timings move with the machine. ±1 render on an action is noise (a feed tick or
|
|
38
|
+
a poll landing on a keystroke); a result is clear when it moves more, or the same way on a second run.
|
|
39
|
+
- Read `warnings` first: a different page, area or a partial side makes it no comparison.
|
|
40
|
+
- Roots that appeared and those that are gone say whether the work went away or moved; `match: 'name'` finds a root
|
|
41
|
+
the fix moved in the tree.
|
|
42
|
+
- The answer: the change, the scenario, the conditions, the numbers that moved, and what else could have moved them.
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
# The panel
|
|
2
|
+
|
|
3
|
+
What to tell a person who records it themselves.
|
|
4
|
+
|
|
5
|
+
- **Alt+Shift+R** records and stops, **Alt+Shift+S** picks an area. **↺ Page load** reloads and records from the
|
|
6
|
+
first render.
|
|
7
|
+
- **⌖ Pick** — click an element, or a row of the tree it opens, and it becomes the area, with the component tree
|
|
8
|
+
open around it. `↑`/`↓` move, `→` goes in, `←` goes up, `Enter` keeps it, `Esc` puts the old one back. The page
|
|
9
|
+
does not react to clicks while the picker is open. `×` goes back to the whole app.
|
|
10
|
+
- **⧉** copies the area as text for a chat: component, file and line, path, DOM, and the `scope` for a script — what
|
|
11
|
+
a person sends instead of "that panel on the right".
|
|
12
|
+
- **highlights** outlines renders inside the area, live. Off for timing runs: a recording made with it on carries a
|
|
13
|
+
warning, and `compare_recordings` complains when only one side had it.
|
|
14
|
+
- **fast** records lists of thousands at about half the cost; reasons of parent-caused renders are then a sample.
|
|
15
|
+
- **◎** in a tree row follows a component by name through the recording.
|
|
16
|
+
- While recording, the panel names the roots leading so far; after Stop it shows the report and `saved <id>`.
|
|
17
|
+
- In the report, a commit picked on the timeline shows its cascade as a tree and outlines its components on the page;
|
|
18
|
+
**↻ Repeat** reloads and does the same actions again, for a before → after.
|
|
19
|
+
|
|
20
|
+
The panel is hidden in automated browsers (`navigator.webdriver`) unless the URL has `?rpr=panel`; the shortcuts
|
|
21
|
+
work either way.
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
# Reading a recording
|
|
2
|
+
|
|
3
|
+
In the order a diagnosis needs: who started the cascade, why it rendered, how it reached the component, and how much
|
|
4
|
+
of it was wasted. What scheduled the commit: `causes-and-actions.md`.
|
|
5
|
+
|
|
6
|
+
## Roots
|
|
7
|
+
|
|
8
|
+
A **cascade root** is a component that rendered while its parent did not — where a render started. `hits` is how
|
|
9
|
+
many commits it started, `cascade` the renders it pulled, `perHit` the renders per commit, `instances` how many
|
|
10
|
+
copies fired at once. `outsideRoots` are roots above the recorded area that reached into it.
|
|
11
|
+
|
|
12
|
+
`noDomChange` (per root) and `rendersWithoutDom` (in totals) count renders after which the DOM did not change —
|
|
13
|
+
waste with no argument attached. `mounts` other than zero on a page that only changes text means remounting: a
|
|
14
|
+
component declared inside a render, or an unstable `key`.
|
|
15
|
+
|
|
16
|
+
## Reasons
|
|
17
|
+
|
|
18
|
+
- `state now` (the name the code gives it; `#2` when it is not known), `external store #3 [useStore] selectPrice`,
|
|
19
|
+
`context Theme`, `props: value | same: style, onClick`;
|
|
20
|
+
- `SAME-CONTENT` — a new reference with the same content: almost always a subscription that asks for more than it
|
|
21
|
+
shows, not new data;
|
|
22
|
+
- `bailout: state set to the same value` — React called the component and threw the result away.
|
|
23
|
+
|
|
24
|
+
**The hook chain** turns the reason into the code that owns it:
|
|
25
|
+
|
|
26
|
+
```
|
|
27
|
+
state #2 SAME-CONTENT · useSLTPInput › [react-hook-form] useController › useFormState › State
|
|
28
|
+
@ src/order/SLTPInput.tsx:48 const { fieldState } = useController(…)
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
`[package]` is the border: to its left the app's own hooks — where a fix goes — to its right the library's insides.
|
|
32
|
+
In an `external store` reason, `[useStore]` names the store and what follows is the selector.
|
|
33
|
+
|
|
34
|
+
## Components and their ways
|
|
35
|
+
|
|
36
|
+
`section: components` has a reason per component, parent-caused renders included:
|
|
37
|
+
|
|
38
|
+
- `parent: props price | same: style, onClick` — what broke `memo`, and which props were only new references;
|
|
39
|
+
- `parent: props equal` — a `memo` would have skipped this render;
|
|
40
|
+
- `chains` — up to three ways its renders came down, as `way`: the root's leading cause, the root and its reason,
|
|
41
|
+
then the props each parent handed on:
|
|
42
|
+
`react-query:fetch ["presence"] › Stats · state online › Line · prop online › Badge · prop count`. The first link
|
|
43
|
+
with `props equal` is where a `memo` stops the rest of the way. Up to 20 links; none in fast recordings.
|
|
44
|
+
|
|
45
|
+
`section: timeline` gives a commit whose roots rendered anyone a `cascade`: that commit's tree as indented lines,
|
|
46
|
+
busiest branch first — who rendered whom, through which props.
|
|
47
|
+
|
|
48
|
+
`library` / `wrapper` mark a package's component and an unnamed one (`Anonymous`, `Memo`); the app's own come first.
|
|
49
|
+
|
|
50
|
+
## Memos
|
|
51
|
+
|
|
52
|
+
`memos` names a `useMemo` or `useCallback` that recomputed on at least half of its renders: which dependency moved
|
|
53
|
+
(by its name in the code when it can be read), whether into the same content, and the line. "A new object with the
|
|
54
|
+
same content every time" is a dependency written in render: make it once — a constant, or its own `useMemo` — rather
|
|
55
|
+
than adding another memo.
|