ENG-12682 Let React Compiler optimize generated components
Supersedes #7272. That PR was stacked on #7234 and auto-closed, unmerged, when its base branch was deleted after #7234 merged. This PR replays its changes onto current main: the diff between the base-branch head it last merged (07aa53cb6) and its head (4f3e5dbba). Farhan is the commit author.
All Submissions:
- Have you followed the guidelines stated in CONTRIBUTING.md file?
- Have you checked to ensure there aren't any other open Pull Requests for the desired changed?
Type of change
- New feature (non-breaking change which adds functionality)
- This change requires a documentation update (included:
docs/advanced_onboarding/configuration.md)
Changes
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.
| 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 | moduleTypeProvider describing jsx like JSX syntax: frozen arguments, frozen result |
The experimental compiler (0.0.0-experimental-a1856f3-20260507) is required, because 1.0.0 still rejects refs passed to jsx() and optional calls in ternary tests. This is still npm's current experimental tag, and REACT_COMPILER_DEV_DEPENDENCIES is the only pin; @babel/core stays at main's 7.29.7.
auto_memoize=False removes MemoizeStatefulPlugin from page compilation. App-root memoization (_memoize_app_root) is unchanged.
Conflicts with main and how they were resolved
The patch applied cleanly everywhere except two hunks:
component.py_get_ref_hook: #7586 removed hook positions, so main's ref hook no longer passesVarData(position=HookPosition.INTERNAL). The resolution keepsuseRegisteredRefand drops the position.state.js: the PR's hunk carriedlet backend_state_mismatch = false;as context, but #7435 removed that from main. The resolution adds onlyuseRegisteredRef.
I also checked the memo-compiler changes on main that the patch didn't textually touch: mirrored memo modules, auto-memo keying in memoize.py, and compiler.py. default_page_plugins is still the only place page compilation adds MemoizeStatefulPlugin, so the auto_memoize switch still covers every page path.
Measurements (from the original PR #7272)
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 |
Python compile time with auto-memo off (from #7272):
| 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 |
Testing
-
uv run ruff check .,uv run ruff format .,uv run pyright reflex tests(0 errors), anduv run pre-commit run --all-filesall pass. The.pyihook made no changes. -
tests/units/compiler,tests/units/reflex_base,test_prerequisites.py,test_config.py, andcomponents/test_component.py: 1943 passed. - Full unit suite: 15274 passed. The only failures were the 14
reflex_bench/drivers/test_browser.pytests, because the dev container lacks the Playwright headless-shell binary those tests need. That is unrelated to this diff. - Smoke app with
react_compiler=True, auto_memoize=Falseunderreflex run --env prod:- It compiles and builds, with no memo wrappers generated.
- The page chunk is compiler-transformed (memo cache sentinels are present).
- In Chromium, three backend increments,
rx.set_focusthroughuseRegisteredRef, and a controlled-input round trip all worked, with no page errors.
Known limits (from #7272)
- 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.
https://claude.ai/code/session_01LwJUCTv3FVWzi1UoUpyMLT
Generated by Claude Code