perf: disable React dev-build owner-stack capture in dev mode
Why dev mode feels slow to click around
Profiling real navigation clicks in a large Reflex app (the reflex.dev docs site, 1,023 pages) under reflex run vs --env prod, interleaved click-by-click on the same machine:
| busy CPU per navigation click | |
|---|---|
reflex run (dev) | 345.7 ms |
--env prod | 62.6 ms |
71% of the dev CPU (246.7 ms) is a single function: React's development-build createElement. React 19's dev runtime constructs an Error("react-stack-top-frame") plus a console.createTask() for every element created, to capture "owner stacks" for DevTools. Two things make this brutal in practice:
- The cost scales with render depth.
Error()walks the live JS stack: measured 2.1 µs/element at stack depth 2, but 12–17 µs at the depth React actually renders at. A docs page creates ~10,300 elements per client-side navigation → ~247 ms. - Humans pay full price on every click. React budgets the capture to 10k elements, but
react-domresets the counter 1 s after the last render. Tight automated loops exhaust the budget once and look fast; a person clicking at a normal pace gets a fresh budget — and the full cost — every single click. This is exactly the "dev feels sluggish but benchmarks look fine" signature.
(The "outdated JSX transform" warning is a red herring here — the modern jsx/jsxs runtime executes the identical per-element capture.)
The fix
Emit a small dev-only snippet in the generated context.js that pins React's budget counter past its cap, so element creation takes the runtime's own cheap branch (unknownOwnerDebugStack). A pinned accessor is required — a plain write would be undone by react-dom's once-per-second reset (this is also why naive attempts to disable it appear to do nothing).
Everything else about the dev build is unchanged: dev warnings, component stacks in errors, StrictMode behavior, HMR/Fast Refresh (all re-verified against a live app, including in-place HMR). The trade-off: elements carry React's shared unknown-owner placeholder, so the public React.captureOwnerStack() returns no owner frames in dev — affecting React DevTools' owner-stack view and anything built on that API, such as custom error overlays and the onCaughtError/onUncaughtError root handlers (verified against the pinned React 19.2.8). REFLEX_REACT_OWNER_STACKS=1 restores full owner stacks; the guard is browser-only, so SSR diagnostics are untouched. The snippet is defensive (existence-checked, try/catch) so a future React that drops the internal is a no-op, and it is not emitted in prod builds at all.
Measured result
Interleaved A/B on one server boot (docs app, human-paced navigation clicks, CDP CPU profiles, median of n=10 per arm):
| busy CPU / nav | createElement self time | settle time | |
|---|---|---|---|
| unpinned (today's dev) | 353.1 ms | 250.3 ms | 348.4 ms |
| pinned (this PR) | 82.8 ms | 1.0 ms | 81.8 ms |
−270 ms per click (−77%), taking dev from 5.6× prod to ~1.3× prod. Verified no console errors, navigation, state events, and hot reload working on both a minimal app and the docs app; full unit suite passes (7,460).