ENG-12682 Let React Compiler optimize generated components
Stacked on #7234.
React Compiler bailed out on most generated pages and memo wrappers because of patterns Reflex emits. This PR changes those patterns. It also adds an auto_memoize option, so an app using react_compiler can let the compiler memoize whole pages instead of Reflex's generated memo wrappers.
Changes
| Bailout | Fix |
|---|---|
refs["ref_x"] = ref_x during render ("This value cannot be modified") | const ref_x = useRegisteredRef("ref_x"). The hook lives in utils/state.js, outside compiler scope, and keeps the same registry behavior. |
useMemo(generateUUID, []) ("Expected the first argument to be an inline function expression") | useMemo(() => generateUUID(), []) |
useCallback deps list addEvents, ReflexEvent, and unused hook bindings such as setFilesById ("Found extra memoization dependencies") | Only bindings the callback references |
Capture-free (...args) => addEvents(...) handlers crash function outlining ("Expected temporaries to be promoted") | enableFunctionOutlining: false |
jsx(type, props, ...children) from Emotion is an unknown function, so compiled pages cannot cache elements (33 vs 280 cache slots on the test dashboard) | moduleTypeProvider describing jsx like JSX syntax: frozen arguments, frozen result |
The experimental compiler (0.0.0-experimental-a1856f3-20260507) is required. 1.0.0 still rejects refs passed to jsx() and optional calls in ternary tests.
auto_memoize=False removes MemoizeStatefulPlugin from page compilation. App-root memoization is unchanged.
Measurements
Production builds, Chromium, direct state dispatch, 11 alternating rounds × 100 updates. Dashboard page: sidebar, 8 KPI cards, two Recharts charts, tabs with a form and a 10-item list, static cards.
| Update | Today | react_compiler | react_compiler + auto_memoize=False |
|---|---|---|---|
| One KPI value | 0.87 ms, 171 renders | 0.095 ms, 28 renders | 0.106 ms, 10 renders |
| Search input | 0.99 ms | 0.144 ms | 0.163 ms |
| Chart data | 5.90 ms | 5.19 ms | 5.06 ms |
| Activity list | 0.89 ms | 0.33 ms | 0.42 ms |
| Header select | 1.06 ms | 0.27 ms | 0.30 ms |
Compiler page coverage with auto_memoize=False: 17/17 stateful pages in a feature fixture, plus 7/8 in a built-in component sweep. The sweep covers Radix, markdown, code, data editor, gridjs, Recharts, Plotly, moment, and event patterns. rx.auto_scroll still bails because its hook code mutates a ref value in render-scope helpers. The app-root Toaster still writes refs['__toast'] during render, and it renders once.
Python compile time with auto-memo off:
| App | Auto-memo on | Auto-memo off |
|---|---|---|
| Dashboard fixture | 200 ms | 128 ms |
| Feature fixture | 179 ms | 144 ms |
| Docs site | 42.2 s | 31.7 s |
Validation
- Unit suite: 9,731 passed, 19 skipped, 79% coverage. ruff, pyright, and pre-commit pass.
- Browser checks on both new variants passed with no page errors:
rx.set_focus, controlled input round trip, and backend events. The rendered DOM is identical between compiler variants.
Known limits
- With
auto_memoize=False, a page the compiler skips re-renders fully on every state change, about 2× slower than today. The docs say to measure before turning it off. - Custom components that inject raw hook JavaScript can still cause bailouts.