react-x11 2.51.0 → 2.52.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/README.md +87 -54
- package/docs/README.md +410 -0
- package/docs/accessibility.md +628 -0
- package/docs/appearance.md +416 -0
- package/docs/architecture/accessibility.md +470 -0
- package/docs/architecture/animation.md +707 -0
- package/docs/architecture/container-queries.md +525 -0
- package/docs/architecture/custom-layout.md +334 -0
- package/docs/architecture/drag-and-drop.md +941 -0
- package/docs/architecture/element-layer-contents.md +622 -0
- package/docs/architecture/frame-pacing.md +255 -0
- package/docs/architecture/grid-layout.md +358 -0
- package/docs/architecture/protocol-efficiency.md +357 -0
- package/docs/architecture/video.md +1149 -0
- package/docs/architecture/windowed-regions.md +854 -0
- package/docs/click-to-component.md +125 -0
- package/docs/clipboard.md +281 -0
- package/docs/components.md +1749 -0
- package/docs/dbus.md +277 -0
- package/docs/debugging.md +464 -0
- package/docs/desktop-calendar.md +284 -0
- package/docs/desktop.md +648 -0
- package/docs/devtools.md +250 -0
- package/docs/drag-and-drop.md +663 -0
- package/docs/ecosystem/animation.md +394 -0
- package/docs/ecosystem/data-fetching.md +201 -0
- package/docs/ecosystem/dev-tooling.md +436 -0
- package/docs/ecosystem/forms.md +313 -0
- package/docs/ecosystem/headless.md +219 -0
- package/docs/ecosystem/i18n.md +89 -0
- package/docs/ecosystem/icons.md +410 -0
- package/docs/ecosystem/layout.md +216 -0
- package/docs/ecosystem/routing.md +146 -0
- package/docs/ecosystem/state.md +361 -0
- package/docs/ecosystem/testing.md +500 -0
- package/docs/ecosystem/theming.md +540 -0
- package/docs/ecosystem.md +473 -0
- package/docs/elements.md +2762 -0
- package/docs/embedding.md +288 -0
- package/docs/events.md +1025 -0
- package/docs/extending.md +2242 -0
- package/docs/eyedropper.md +223 -0
- package/docs/filedialog.md +258 -0
- package/docs/frame.md +580 -0
- package/docs/gl.md +170 -0
- package/docs/globalmenu.md +278 -0
- package/docs/glx.md +137 -0
- package/docs/macos.md +3295 -0
- package/docs/notifications.md +164 -0
- package/docs/packaging.md +611 -0
- package/docs/permissions.md +148 -0
- package/docs/react-features.md +430 -0
- package/docs/remote.md +223 -0
- package/docs/scale.md +339 -0
- package/docs/security.md +132 -0
- package/docs/styling.md +1928 -0
- package/docs/system.md +599 -0
- package/docs/testing.md +514 -0
- package/docs/typescript.md +117 -0
- package/docs/uri-schemes.md +380 -0
- package/docs/wayland-backend.md +591 -0
- package/docs/wayland.md +545 -0
- package/docs/windows-embedding.md +271 -0
- package/docs/windows-gl.md +221 -0
- package/docs/windows-integrations.md +233 -0
- package/docs/windows.md +1404 -0
- package/package.json +10 -3
- package/src/bus.js +39 -26
- package/src/cli/create.d.ts +43 -0
- package/src/cli/create.js +305 -0
- package/src/cli/env.js +484 -0
- package/src/cli/index.js +29 -2
- package/src/cli/templates/AGENTS.template.md +195 -0
- package/src/cli/templates/DESIGN.template.md +269 -0
- package/src/desktopintegration.js +2 -1
- package/src/desktopsettings.js +193 -24
- package/src/filedialog.js +3 -3
- package/src/keyboardstate.js +28 -0
- package/src/node.d.ts +10 -10
- package/src/portal.js +31 -0
- package/src/screencolor.js +5 -10
- package/src/types/system.d.ts +8 -6
- package/src/wayland/app.js +59 -0
- package/src/wayland/clipboard.js +14 -7
- package/src/wayland/connection.js +3 -0
- package/src/wayland/context2d.js +129 -10
- package/src/wayland/protocols/index.json +6 -0
- package/src/wayland/protocols/xdg-foreign-unstable-v2.json +1 -0
- package/src/wayland/seat.js +28 -1
- package/src/wayland/swapchain.js +23 -0
- package/src/wayland/xkb.js +137 -0
|
@@ -0,0 +1,436 @@
|
|
|
1
|
+
# Developer tooling
|
|
2
|
+
|
|
3
|
+
This is the payoff of a pure-JS stack: a react-x11 app is an ordinary Node
|
|
4
|
+
process, so the whole V8 and React tooling story applies unmodified. Nothing
|
|
5
|
+
on this page needs an adapter.
|
|
6
|
+
|
|
7
|
+
The four tools answer four different questions, and it is worth knowing which
|
|
8
|
+
is which before you reach for one:
|
|
9
|
+
|
|
10
|
+
| Question | Tool |
|
|
11
|
+
| ---------------------------------------------- | ------------------------------------------------ |
|
|
12
|
+
| Which component rendered, and with what props? | [React DevTools](#react-devtools) |
|
|
13
|
+
| Was that render _wasted_? | [why-did-you-render](#why-did-you-render) |
|
|
14
|
+
| Where did the milliseconds go? | [`node --inspect`](#node-inspect), [`0x`](#0x) |
|
|
15
|
+
| Why is memory growing? | [`node --inspect`](#node-inspect) heap snapshots |
|
|
16
|
+
|
|
17
|
+
`react-scan` is not on this page: its documented entry point does not resolve
|
|
18
|
+
under bare Node, and its programmatic API silently never fires. See the
|
|
19
|
+
[negative-results register](../ecosystem.md#silent-failures). React's own
|
|
20
|
+
`<Profiler>` works unchanged under react-x11 and covers the same ground for
|
|
21
|
+
commit timings.
|
|
22
|
+
|
|
23
|
+
## React DevTools {#react-devtools}
|
|
24
|
+
|
|
25
|
+
**Out of the box — already wired behind an env var.** react-devtools-core@7.0.1.
|
|
26
|
+
|
|
27
|
+
`react-devtools-core` is the backend half of React DevTools, made for exactly
|
|
28
|
+
this situation: a React tree running outside a browser page. It connects over
|
|
29
|
+
WebSocket to the standalone DevTools UI and streams the fiber tree, props,
|
|
30
|
+
hooks and profiler data. The integration lives in
|
|
31
|
+
`src/DevToolsIntegration.js` and costs nothing when off.
|
|
32
|
+
|
|
33
|
+
```sh
|
|
34
|
+
npm i -D react-devtools-core
|
|
35
|
+
|
|
36
|
+
# terminal 1 — standalone UI, listens on :8097
|
|
37
|
+
npx react-devtools
|
|
38
|
+
|
|
39
|
+
# terminal 2 — any app
|
|
40
|
+
REACT_X11_DEVTOOLS=1 npm run examples:dashboard
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
`src/Reconciler.js` checks `REACT_X11_DEVTOOLS` when you first call
|
|
44
|
+
`render()`/`createRoot()`, and installs the DevTools global hook _before the
|
|
45
|
+
first commit_ — that ordering is what makes mounted roots visible, and it
|
|
46
|
+
holds because a commit can only follow a root.
|
|
47
|
+
`REACT_X11_DEVTOOLS_HOST` and `REACT_X11_DEVTOOLS_PORT` point the backend at
|
|
48
|
+
a UI elsewhere, which is useful when the app runs on a headless box next to
|
|
49
|
+
the X server.
|
|
50
|
+
|
|
51
|
+
Working today: the live component tree, props/hooks/state inspection, and
|
|
52
|
+
highlight-on-hover — hovering an element in the tree tints its rect in the
|
|
53
|
+
X11 window. See [devtools](../devtools.md) for the full walkthrough.
|
|
54
|
+
|
|
55
|
+
- **Start the standalone UI first**, or wait for the backend's retry. The app
|
|
56
|
+
side retries; the UI side does not attach to an already-running app unless
|
|
57
|
+
the hook was installed at startup, and `REACT_X11_DEVTOOLS` cannot be
|
|
58
|
+
turned on after the fact.
|
|
59
|
+
- The Profiler's commit-oriented views (flamegraph, ranked) work with the dev
|
|
60
|
+
reconciler. The **Timeline** view cannot — it needs `injectProfilingHooks`,
|
|
61
|
+
which react-reconciler 0.33 does not expose at all.
|
|
62
|
+
- `injectIntoDevTools` takes zero arguments in react-reconciler 0.33, so the
|
|
63
|
+
config object passed to it is silently discarded and
|
|
64
|
+
`findFiberByHostInstance` is never registered — DevTools cannot map a host
|
|
65
|
+
node back to its fiber. The tree, props and highlight features do not need
|
|
66
|
+
it. Do not add options to that call expecting them to do anything; the
|
|
67
|
+
supported channel is `extraDevToolsConfig` on the host config.
|
|
68
|
+
|
|
69
|
+
## react-refresh {#react-refresh}
|
|
70
|
+
|
|
71
|
+
**Supported — `react-x11 dev` is the command, and `react-x11/refresh` the
|
|
72
|
+
entry point under it.** Fast Refresh without a bundler: edit a component,
|
|
73
|
+
and the mounted window updates in place with hook state intact. The X
|
|
74
|
+
connection, the window, the task list, half-typed text in an input — all
|
|
75
|
+
survive.
|
|
76
|
+
|
|
77
|
+
```sh
|
|
78
|
+
npx react-x11 dev app.jsx
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
A new app comes set up for it: `npm create react-x11@latest my-app` (or
|
|
82
|
+
`npx react-x11 create my-app`) writes `src/App.jsx`, an entry for a plain
|
|
83
|
+
run, and a `dev` script, with the toolchain below already in its
|
|
84
|
+
devDependencies. Beside them go `DESIGN.md`, the app's design brief, and
|
|
85
|
+
`AGENTS.md`, which tells a coding agent how react-x11 differs from
|
|
86
|
+
react-dom, where the docs installed with the package are, and how to check
|
|
87
|
+
its work by rendering the app to a PNG in light and dark
|
|
88
|
+
(`--no-agents-md` leaves it out).
|
|
89
|
+
|
|
90
|
+
### `react-x11 dev` {#react-x11-dev}
|
|
91
|
+
|
|
92
|
+
The package's `bin`, and the route with nothing to set up. What it does,
|
|
93
|
+
each because the bare loader leaves it to the app:
|
|
94
|
+
|
|
95
|
+
- **It is the entry, and imports the file.** The file named on node's
|
|
96
|
+
command line is the one module hot-module-replacement never watches (see
|
|
97
|
+
below), so here the file is imported instead, which makes it an ordinary
|
|
98
|
+
hot module: an app in one file reloads.
|
|
99
|
+
- **It mounts the file's default export.** A file that mounts a root of its
|
|
100
|
+
own while it is imported runs as it is — `dev` sees the root appear and
|
|
101
|
+
mounts nothing — and then the modules it imports reload while it does
|
|
102
|
+
not. A file that runs both ways mounts itself behind
|
|
103
|
+
`if (!import.meta.hot)`, which is false under `dev` and true under `node`,
|
|
104
|
+
`tsx` and `bun`; `examples/tasks.jsx` is written that way. One that mounts
|
|
105
|
+
itself unguarded _and_ exports only components is told, at start, that
|
|
106
|
+
every save re-runs its `createRoot` and opens another window.
|
|
107
|
+
- **It supplies a `<window>` while the tree renders none.** The tree is
|
|
108
|
+
mounted as it is; a drawn element at the root throws an error with
|
|
109
|
+
`code: 'NOT_IN_WINDOW'` before anything commits, and on that `dev` mounts
|
|
110
|
+
it again inside a window titled after the file, sized to its content with
|
|
111
|
+
a 320×200 floor. Decided again on every reload: render a `<window>` and
|
|
112
|
+
`dev`'s goes (the component remounts, so its state resets — `dev` says
|
|
113
|
+
so), lose it and it comes back. A tree that renders a `<window>` _beside_
|
|
114
|
+
content in none stays wrapped, with a note saying which content needs a
|
|
115
|
+
window. Closing the window ends the process, as closing an app's primary
|
|
116
|
+
window does.
|
|
117
|
+
- **It says when a save will not reload.** A module reloads in place only
|
|
118
|
+
when everything it exports is a component by React Refresh's rule — a
|
|
119
|
+
capitalized name — so `export default () => …` and a module exporting a
|
|
120
|
+
constant beside its component run, but saving them reloads nothing. `dev`
|
|
121
|
+
says so, and why, when it starts, instead of leaving the first save to
|
|
122
|
+
find out.
|
|
123
|
+
- **It asks before it installs the toolchain.** The four packages below are
|
|
124
|
+
optional peer dependencies, so an app's users never download them; `dev`
|
|
125
|
+
finds the ones missing, pins them to the ranges react-x11 is written
|
|
126
|
+
against, and runs the project's package manager — from its lockfile, else
|
|
127
|
+
the one that launched `dev`, else npm — when you say yes, then starts
|
|
128
|
+
again. With no terminal to ask on it prints the command and exits.
|
|
129
|
+
- **It connects to [React DevTools](#react-devtools)** whenever
|
|
130
|
+
`npx react-devtools` is running, started before the app or after it —
|
|
131
|
+
wherever `react-devtools-core` is installed. Where it is not, and a
|
|
132
|
+
DevTools UI is listening when `dev` starts, `dev` prints the install
|
|
133
|
+
command. `REACT_X11_DEVTOOLS=0` turns it off.
|
|
134
|
+
- **It turns on [click-to-component](../click-to-component.md).**
|
|
135
|
+
Alt+Click (Option+Click on macOS) an element and its JSX line opens in
|
|
136
|
+
your editor. `REACT_X11_CLICK_TO_COMPONENT=0` turns it off.
|
|
137
|
+
- **It runs the project's react-x11.** A second copy of the renderer is a
|
|
138
|
+
root nobody refreshes, so a global or npx-cached `react-x11` hands over to
|
|
139
|
+
the copy the file imports, and a file that imports none is told to
|
|
140
|
+
install it.
|
|
141
|
+
|
|
142
|
+
**Errors are shown in a window**, as well as printed:
|
|
143
|
+
|
|
144
|
+
- **A render error** takes the app's tree down, as it would anywhere, and a
|
|
145
|
+
window titled `app.jsx — error` stands in for it. It shows the message, the
|
|
146
|
+
components it was thrown under, the source around the line, and the
|
|
147
|
+
stack's frames — React's, Node's and react-x11's own counted rather than
|
|
148
|
+
listed. The next save tries the app again; the save that fixes it brings
|
|
149
|
+
it back, and an app that throws again is back in the window.
|
|
150
|
+
- **A save that does not compile** opens the same window beside the app,
|
|
151
|
+
which keeps running the module's last version with its state; it closes
|
|
152
|
+
once the file compiles, or when you close it.
|
|
153
|
+
- A line in it opens in your editor on a click, the way
|
|
154
|
+
[click-to-component](../click-to-component.md) does, by
|
|
155
|
+
`REACT_X11_EDITOR` (`cursor` when it is unset).
|
|
156
|
+
- A throw from an event handler leaves the app running, so it is only
|
|
157
|
+
printed, and dispatch continues.
|
|
158
|
+
|
|
159
|
+
Stack lines point at the source's own lines, through every loader layer's
|
|
160
|
+
source map; `dev` turns source maps on for you. Arguments after the file
|
|
161
|
+
are the app's: `react-x11 dev app.jsx --size large` hands the app the
|
|
162
|
+
`process.argv` that `node app.jsx --size large` would. `.jsx` files are
|
|
163
|
+
compiled and hot; `.js` and `.mjs` run as Node runs them, and the `.jsx`
|
|
164
|
+
modules they import are hot. TypeScript is not supported yet.
|
|
165
|
+
|
|
166
|
+
### Under `--import` {#refresh-import}
|
|
167
|
+
|
|
168
|
+
`dev` hosts the loader in its own process; a tool that hosts it itself —
|
|
169
|
+
a workbench, a custom dev loop — registers it with `--import`:
|
|
170
|
+
|
|
171
|
+
```sh
|
|
172
|
+
node --enable-source-maps --import react-x11/refresh/register main.jsx
|
|
173
|
+
```
|
|
174
|
+
|
|
175
|
+
The entry — the file named on the command line — is the one module that is
|
|
176
|
+
**never reloaded**: hot-module-replacement starts watching a module when
|
|
177
|
+
another module imports it, and nothing imports the entry. So an app written
|
|
178
|
+
as one file does not reload at all, and nothing says so. Keep the entry to
|
|
179
|
+
mounting, and the components in modules it imports:
|
|
180
|
+
|
|
181
|
+
```jsx
|
|
182
|
+
// main.jsx — written once, never edited while the app runs
|
|
183
|
+
import React from 'react';
|
|
184
|
+
import ReactX11 from 'react-x11'; // default import: see the rules below
|
|
185
|
+
import App from './app.jsx'; // `export default function App() { ... }`
|
|
186
|
+
|
|
187
|
+
const root = await ReactX11.createRoot();
|
|
188
|
+
root.render(<App />);
|
|
189
|
+
```
|
|
190
|
+
|
|
191
|
+
A save that does not compile is otherwise only printed — hot-module-replacement
|
|
192
|
+
catches the error, and the app keeps the module's last version — so a host
|
|
193
|
+
that wants to show it subscribes to the loader:
|
|
194
|
+
|
|
195
|
+
```js
|
|
196
|
+
import { onTransform } from 'react-x11/refresh/loader';
|
|
197
|
+
|
|
198
|
+
onTransform(({ filename, error }) => {
|
|
199
|
+
// error is null when filename compiled
|
|
200
|
+
});
|
|
201
|
+
```
|
|
202
|
+
|
|
203
|
+
A tool that needs the loader's options writes its own two-line `--import`
|
|
204
|
+
module instead of `react-x11/refresh/register`:
|
|
205
|
+
|
|
206
|
+
```js
|
|
207
|
+
// tool-register.mjs, then: node --import ./tool-register.mjs app.jsx
|
|
208
|
+
import { registerRefresh } from 'react-x11/refresh/loader';
|
|
209
|
+
|
|
210
|
+
await registerRefresh({
|
|
211
|
+
extensions: ['.jsx'], // which files are hot modules
|
|
212
|
+
ignore: (path) => path.includes('/stores/'), // identity that must survive
|
|
213
|
+
prelude: ['globalThis.__TOOL__ = true'], // injected into every hot module
|
|
214
|
+
});
|
|
215
|
+
```
|
|
216
|
+
|
|
217
|
+
### How it reloads {#refresh-rules}
|
|
218
|
+
|
|
219
|
+
Both routes run the same loader. It instruments every `.jsx`
|
|
220
|
+
module and decides per module whether it is a **refresh boundary**: a
|
|
221
|
+
module whose exports are all components self-accepts, so an edit to it (or
|
|
222
|
+
to anything below it with no boundary of its own) re-evaluates up to that
|
|
223
|
+
module and re-renders the edited components in place. A component whose
|
|
224
|
+
hook signature changed remounts alone. There are no accept handlers to
|
|
225
|
+
write; the one optional seam is the reload event:
|
|
226
|
+
|
|
227
|
+
```js
|
|
228
|
+
import { onReload } from 'react-x11/refresh';
|
|
229
|
+
|
|
230
|
+
onReload(({ urls, refreshed }) => {
|
|
231
|
+
console.log(`${urls.length} module(s) reloaded`);
|
|
232
|
+
});
|
|
233
|
+
```
|
|
234
|
+
|
|
235
|
+
Node 22.15 or newer is required, for synchronous `module.registerHooks`,
|
|
236
|
+
and the toolchain rides four optional peer dependencies, which `dev` offers
|
|
237
|
+
to install and `--import` expects installed:
|
|
238
|
+
|
|
239
|
+
```sh
|
|
240
|
+
npm install --save-dev @babel/core @babel/plugin-transform-react-jsx react-refresh hot-module-replacement
|
|
241
|
+
```
|
|
242
|
+
|
|
243
|
+
Under the hood two loader layers stack: babel (classic JSX transform plus
|
|
244
|
+
`react-refresh/babel`) instruments the components, and above it
|
|
245
|
+
`hot-module-replacement` rewrites static imports into live bindings and
|
|
246
|
+
wires `import.meta.hot`. The runtime half patches the DevTools global hook
|
|
247
|
+
before the renderer registers with it — ordering the loader guarantees by
|
|
248
|
+
injecting the runtime import into every hot module's prelude. The hot
|
|
249
|
+
boundary excludes `node_modules` and react-x11's own sources, so React,
|
|
250
|
+
the renderer and `react-refresh/runtime` stay singletons.
|
|
251
|
+
|
|
252
|
+
Each layer puts lines of its own above a module — babel's the refresh
|
|
253
|
+
prelude, hot-module-replacement's a preamble — and each says so in its source
|
|
254
|
+
map and composes the map of the layer beneath it. So a stack through a hot
|
|
255
|
+
module points at the source's own line and column, which is what
|
|
256
|
+
click-to-component opens: run Node with `--enable-source-maps`, which
|
|
257
|
+
`dev` turns on itself. The composing half is hot-module-replacement
|
|
258
|
+
4.0.2's, which is why that is the floor; `test/refresh-stack.test.js` fails
|
|
259
|
+
on 4.0.1.
|
|
260
|
+
|
|
261
|
+
The constraints are enforced errors now, not folklore:
|
|
262
|
+
|
|
263
|
+
- **A named import called at module top level inside a hot module is a
|
|
264
|
+
transform error.** Named imports become live bindings initialized in a
|
|
265
|
+
microtask, so at module scope the value is still undefined. Use the
|
|
266
|
+
default import at top level (`React.createContext(...)`, not
|
|
267
|
+
`createContext(...)`); inside components anything goes. The entry is
|
|
268
|
+
transformed like the rest, so a named `createRoot` import followed by a
|
|
269
|
+
top-level `await createRoot()` is this error: write
|
|
270
|
+
`ReactX11.createRoot()` there.
|
|
271
|
+
- **Only the classic JSX runtime.** `jsxRuntime: 'automatic'` throws: the
|
|
272
|
+
automatic runtime appends its `react/jsx-runtime` import to the last
|
|
273
|
+
import's line, which the line-oriented import rewrite breaks.
|
|
274
|
+
- **One statement per prelude entry.** A `prelude` entry holding two
|
|
275
|
+
statements or a newline throws, because the import rewrite replaces a
|
|
276
|
+
whole import statement's span on its line.
|
|
277
|
+
|
|
278
|
+
What is _not_ enforced, so it is not rediscovered as a bug:
|
|
279
|
+
|
|
280
|
+
- Under `--import`, an edit to the entry is dropped, whatever the entry
|
|
281
|
+
exports: it is never watched (above), so editing it needs a restart. An
|
|
282
|
+
edit anywhere else reaches the tree only if a boundary sits on its import
|
|
283
|
+
chain; a module whose importers lead to the entry without passing a
|
|
284
|
+
component-only module is dropped the same way. Neither says anything —
|
|
285
|
+
`dev` says the second, at start, for the file it runs.
|
|
286
|
+
- **Module-scope side effects re-run on every reload** of the module and
|
|
287
|
+
of everything between it and its boundary. Keep hot modules'
|
|
288
|
+
top level idempotent, and keep identity that must survive (contexts,
|
|
289
|
+
stores) in modules outside the hot graph — via the `ignore` seam or by
|
|
290
|
+
extension. `registerElement` at module scope is safe: once the first hot
|
|
291
|
+
re-import begins, a duplicate registration replaces silently instead of
|
|
292
|
+
throwing (the
|
|
293
|
+
[re-registration policy](../extending.md#hot-reload)) — mounted nodes keep the old
|
|
294
|
+
prototype until they remount, the same staleness contract Fast Refresh
|
|
295
|
+
has for classes. Remember the named-import rule applies to it like
|
|
296
|
+
anything else: call it as `Host.registerElement(...)` through a
|
|
297
|
+
namespace or default import.
|
|
298
|
+
- Fast Refresh needs the dev reconciler; `NODE_ENV=production` is a
|
|
299
|
+
registration error. There is no production hot path, by design.
|
|
300
|
+
|
|
301
|
+
## `node --inspect` {#node-inspect}
|
|
302
|
+
|
|
303
|
+
**Out of the box.** Node built-in.
|
|
304
|
+
|
|
305
|
+
Node's V8 inspector protocol: `node --inspect` exposes a debug port, and
|
|
306
|
+
Chrome's `chrome://inspect` (or VS Code's auto-attach) connects a full
|
|
307
|
+
DevTools instance — sourcemap-aware CPU profiler, heap snapshots, allocation
|
|
308
|
+
timelines, live debugging.
|
|
309
|
+
|
|
310
|
+
Everything that costs time in a react-x11 app is visible as ordinary JS
|
|
311
|
+
frames, which is why this is the recommended profiling path:
|
|
312
|
+
|
|
313
|
+
- **CPU profiles of commits.** Reconciler frames (`performUnitOfWork`,
|
|
314
|
+
`completeWork`), yoga layout (`calculateLayout`), style resolution and ntk
|
|
315
|
+
paint/flush show up in one call tree. That answers "is it React, layout, or
|
|
316
|
+
painting" in a single recording.
|
|
317
|
+
- **Heap snapshots of retained nodes.** Every drawn element is a retained
|
|
318
|
+
node object reachable from the ntk window's `_reactX11Node`. Snapshot,
|
|
319
|
+
filter the constructor list, and leaked subtrees — a `<popup>` that never
|
|
320
|
+
unmounted, listeners held after close — are directly countable. Two
|
|
321
|
+
snapshots plus "Objects allocated between…" isolates a leak per
|
|
322
|
+
interaction.
|
|
323
|
+
|
|
324
|
+
```sh
|
|
325
|
+
# interactive: profile a running app from Chrome
|
|
326
|
+
node --inspect --import tsx examples/dashboard.jsx
|
|
327
|
+
# chrome://inspect -> Open dedicated DevTools for Node -> Performance / Memory
|
|
328
|
+
|
|
329
|
+
# capture-on-exit CPU profile, no UI needed
|
|
330
|
+
node --cpu-prof --cpu-prof-dir=./profiles --import tsx examples/dashboard.jsx
|
|
331
|
+
|
|
332
|
+
# heap snapshot of a live app without restarting it
|
|
333
|
+
kill -USR1 <pid> # inspector comes up on :9229, then attach and snapshot
|
|
334
|
+
```
|
|
335
|
+
|
|
336
|
+
- **Profile with `NODE_ENV=production` when chasing React costs.** The dev
|
|
337
|
+
reconciler's stack-frame bookkeeping (`runWithFiberInDEV`) inflates commit
|
|
338
|
+
times and clutters the tree. For layout and paint costs it matters less.
|
|
339
|
+
- The inspector port is an arbitrary-code-execution door. Bind it to
|
|
340
|
+
`127.0.0.1` (the default) and never expose it on a network; for remote
|
|
341
|
+
boxes, tunnel it with `ssh -L 9229:localhost:9229`.
|
|
342
|
+
- This and the React DevTools Profiler are complementary: DevTools names the
|
|
343
|
+
_components_ per commit, the V8 profiler names the _functions_.
|
|
344
|
+
|
|
345
|
+
## `0x` {#0x}
|
|
346
|
+
|
|
347
|
+
**Out of the box.** 0x@6.0.0.
|
|
348
|
+
|
|
349
|
+
A single-command flamegraph generator: `0x -- node app.js` runs the process
|
|
350
|
+
under the V8 sampling profiler and, on exit, writes a self-contained
|
|
351
|
+
interactive `flamegraph.html`. No integration at all, and the interesting
|
|
352
|
+
frames are directly searchable — `performUnitOfWork`/`completeWork`, `yoga`,
|
|
353
|
+
and your own components.
|
|
354
|
+
|
|
355
|
+
```sh
|
|
356
|
+
# profile an example, write the output into a throwaway dir
|
|
357
|
+
npx 0x --output-dir /tmp/rx11-prof -- node --import tsx examples/dashboard.jsx
|
|
358
|
+
# interact with the window until the slow thing happens, then Ctrl-C
|
|
359
|
+
# -> file:///tmp/rx11-prof/flamegraph.html
|
|
360
|
+
|
|
361
|
+
# tips inside the flamegraph UI:
|
|
362
|
+
# search "yoga" -> layout cost
|
|
363
|
+
# search "performUnitOfWork" -> react reconcile cost
|
|
364
|
+
# "Merge" view -> collapses recursion for a cleaner ranking
|
|
365
|
+
```
|
|
366
|
+
|
|
367
|
+
**Why 0x and not clinic.js:** Clinic's flame command is a wrapper around 0x,
|
|
368
|
+
so for flamegraphs it adds moving parts without adding signal. Its other
|
|
369
|
+
tools target server event-loop and async-I/O patterns that do not map onto a
|
|
370
|
+
GUI render loop, and the package has been dormant since early 2024 while 0x
|
|
371
|
+
keeps shipping.
|
|
372
|
+
|
|
373
|
+
- Sampling profiler: sub-millisecond handlers need a longer capture to show
|
|
374
|
+
up, so drive the interaction in a loop.
|
|
375
|
+
- Profile with `NODE_ENV=production` when measuring React itself.
|
|
376
|
+
- Running through the tsx loader is fine, though frames from `.jsx` files
|
|
377
|
+
show transformed function names. Component names usually survive.
|
|
378
|
+
- 0x answers "where does CPU time go", and says nothing about _which commit_
|
|
379
|
+
or _why it re-rendered_. Pair it with the DevTools Profiler and
|
|
380
|
+
why-did-you-render. For heap questions use `node --inspect`; 0x is CPU-only.
|
|
381
|
+
|
|
382
|
+
## why-did-you-render {#why-did-you-render}
|
|
383
|
+
|
|
384
|
+
**Out of the box.** @welldone-software/why-did-you-render@10.0.1.
|
|
385
|
+
|
|
386
|
+
Patches the `React` namespace so that components you flag report _avoidable_
|
|
387
|
+
re-renders — props or hook state that changed by reference but are equal by
|
|
388
|
+
value. It hooks React itself, not `react-dom`, which is exactly why it works
|
|
389
|
+
under a custom renderer.
|
|
390
|
+
|
|
391
|
+
```jsx
|
|
392
|
+
import React from 'react';
|
|
393
|
+
import whyDidYouRender from '@welldone-software/why-did-you-render';
|
|
394
|
+
import { createRoot } from 'react-x11';
|
|
395
|
+
|
|
396
|
+
whyDidYouRender(React); // before anything renders
|
|
397
|
+
|
|
398
|
+
const Label = ({ user }) => <text>{`hi ${user.name}`}</text>;
|
|
399
|
+
Label.whyDidYouRender = true;
|
|
400
|
+
|
|
401
|
+
// Every render passes a fresh-but-equal object — the classic wasted render:
|
|
402
|
+
const App = () => (
|
|
403
|
+
<window width={300} height={200} title="wdyr">
|
|
404
|
+
<box style={{ flexGrow: 1 }}>
|
|
405
|
+
<Label user={{ name: 'ada' }} />
|
|
406
|
+
</box>
|
|
407
|
+
</window>
|
|
408
|
+
);
|
|
409
|
+
|
|
410
|
+
(await createRoot()).render(<App />);
|
|
411
|
+
// terminal: "Label … props.user: different objects that are equal by value."
|
|
412
|
+
```
|
|
413
|
+
|
|
414
|
+
- Props tracking reports a re-render caused by a new-but-deep-equal object
|
|
415
|
+
prop with `diffType: 'deepEquals'` and the offending path. Hook tracking
|
|
416
|
+
(`trackHooks`, on by default) reports `setState` with an equal-by-value
|
|
417
|
+
object the same way.
|
|
418
|
+
- The default notifier prints readable `console.group` output in a terminal —
|
|
419
|
+
no browser console needed. A custom `notifier` receives the structured
|
|
420
|
+
`{displayName, reason: {propsDifferences, hookDifferences}}` object, which
|
|
421
|
+
is also how you assert on wasted renders in tests.
|
|
422
|
+
- Works with the automatic JSX runtime as compiled by tsx and esbuild; the
|
|
423
|
+
babel alias the docs describe for webpack setups is not needed.
|
|
424
|
+
- **Install it before components are defined and before the first render** —
|
|
425
|
+
the patch wraps `React.createElement` and the hook entry points on the
|
|
426
|
+
namespace.
|
|
427
|
+
- Dev-only by design. With `NODE_ENV=production` React's prod build drops the
|
|
428
|
+
internals it reads; under plain `node`/`tsx` you get the dev build, which
|
|
429
|
+
is the right mode for it anyway.
|
|
430
|
+
- Updates scheduled outside an event handler flush on a delayed task in React
|
|
431
|
+
19, so in a scripted repro, wait a macrotask before asserting the
|
|
432
|
+
notification fired.
|
|
433
|
+
|
|
434
|
+
This complements React DevTools: DevTools tells you _that_ a component
|
|
435
|
+
rendered; why-did-you-render tells you the render was _wasted_ and which
|
|
436
|
+
value to memoize.
|