Skip to content

[No QA] Upgrade React Compiler to oxc-transform-react 0.145.0 - #98946

Draft
roryabraham wants to merge 3 commits into
mainfrom
rory/oxc-0.145-react-compiler
Draft

[No QA] Upgrade React Compiler to oxc-transform-react 0.145.0#98946
roryabraham wants to merge 3 commits into
mainfrom
rory/oxc-0.145-react-compiler

Conversation

@roryabraham

Copy link
Copy Markdown
Contributor

Explanation of Change

Upgrades the OXC React Compiler used by the web build from oxc-transform@0.136.0 to oxc-transform-react@0.145.0.

As of 0.144.0 the React Compiler no longer ships inside oxc-transform — it moved to a dedicated oxc-transform-react package. oxc-transform@0.145.0 has no reactCompiler option at all, so this is a package swap rather than a version bump. oxc-transform had no other callers in the repo, so it is removed outright.

What changed, and why

  1. config/rsbuild/loaders/oxc-react-compiler-loader.mjs — imports oxc-transform-react. Two things fall out of the new release:

    • The workaround for oxc#23587 is deleted. A Rules-of-React violation used to bail the whole transform and return empty code, so the loader had to re-run the transform with reactCompiler: false. Downgrade nonfatal compiler diagnostics fixed that: the compiler now skips the offending component and still emits code. Measured on src/, 16 files were paying for that second transform.
    • Diagnostic triage now uses severity and the new fatal result flag instead of matching a [ReactCompiler] string prefix (see below).
    • target: 'node20' is dropped — oxc-transform-react has no target option. Verified to be a no-op: transforming all 6,879 src/ files plus 800 files across the included node_modules with and without it produced byte-identical output in every case.
    • cwd is dropped for the same reason. Sourcemap sources entries are unchanged, because the loader passes absolute paths.
  2. config/reactCompiler/checkWithOxc.mjs — the shared analysis helper behind the compliance check and the ESLint processor. Rewritten against the new diagnostics; details in the section below.

  3. Two option defaults changed upstream and are pinned back to current behaviour:

    • eslintSuppressionRules now defaults to ['react-hooks/exhaustive-deps', 'react-hooks/rules-of-hooks'], so a file carrying either suppression opts out of compilation. babel-plugin-react-compiler suppresses that default whenever validateExhaustiveMemoizationDependencies and validateHooksUsage are both on, which is its own default, so web would have silently stopped memoizing 184 files that Metro/Jest still memoize. Set to [].
    • sources now defaults to skipping node_modules. That would drop React Compiler memoization for the INCLUDED_NODE_MODULES allowlist (react-native-web, react-native-reanimated, @react-navigation/*, …) — 59 of 800 sampled files are memoized there today. Set to [''] to keep compiling them.
    • Both are called out because they are defensible the other way: adopting the new defaults would align web with babel.config.js, which already excludes node_modules, and would remove a real Babel/OXC divergence. That is a behaviour change on its own merits, so it is deliberately not bundled into a version upgrade.
  4. isDev is removed from the loader options — it was a real ReactCompilerOptions field in 0.136 and no longer exists in 0.145, where it is silently ignored. Fast Refresh is driven by jsx.refresh, which is unchanged and verified working.

React Compiler Compliance Check

Standardize diagnostics does break the check, and it fails open rather than closed. Both checkWithOxc.mjs and the loader keyed off a [ReactCompiler] message prefix that 0.145 no longer emits, and severity moved too:

0.136.0 0.145.0
Rules-of-React violation Error · [ReactCompiler] Refs: Cannot access refs during render Warning · Cannot access refs during render
Compiler limitation Warning · [ReactCompiler] Todo: … Warning ·

Running the pre-upgrade logic verbatim against 0.145 classifies both RefViolation.tsx and HookOrderViolation.tsx as no-components — no diagnostic matches the prefix, so the OXC half of the check goes permanently green.

The prefix carried a second job: the category (Refs vs Todo) was what separated "this code violates the Rules of React" from "the compiler cannot handle this syntax yet". 0.145 reports both as Warning, and the category now only appears inside the rendered codeframe string. Rather than parse that, checkWithOxc.mjs uses panicThreshold: 'critical_errors', which makes the compiler abort on a real violation (fatal: true, severity Error) while leaving a limitation as a non-fatal Warning. That reproduces the old Error/Warning split from a structured signal, and keeps contributors from being blocked by gaps in the compiler itself. The loader keeps panicThreshold: 'none' so the build never fails on compiler diagnostics.

It also keeps memoized conservative. Under panicThreshold: 'none', 0.145 emits partial memoization for a file where one function violates the rules; didBothCompilersMemoizeFile would then report the file as fully memoized and let the ESLint processor suppress manual-memoization rules that are still needed.

Across all 6,879 src/ files, 25 (0.36%) change classification, all traceable to upstream compiler fixes: 20 move failedno-components (0.136 raised MemoDependencies: Found missing memoization dependencies as a hard error; 0.145 reports Existing memoization could not be preserved as non-fatal), 3 improve to compiled, 1 is newly detected as a genuine ref violation, and 1 gains memoization. None can produce a false CI failure: rule 1 only applies to newly added files, and rules 2 and 3 evaluate the base branch with the same checker, so grandfathering holds.

Performance

Clean production web build, interleaved A/B on one machine (rm -rf node_modules/.cache dist before every run, alternating versions to cancel thermal drift):

Pair 0.136.0 0.145.0 Delta
1 18.52s 17.42s −1.10s
2 18.72s 17.03s −1.69s
3 15.81s 14.48s −1.33s
4 18.68s 18.32s −0.36s
5 20.45s 15.64s −4.81s

Median 18.68s → 17.03s (−8.8%), faster in 5 of 5 pairs. Non-interleaved batches of 3 runs each gave medians of 15.62s → 14.73s. Note the two batches disagree on absolute numbers by ~3s, which is why the paired result is the one worth reading.

The compiler is not the bottleneck in a full production build, so isolating the transform step is more informative. Transforming all 6,879 src/ files single-threaded, 5 iterations, median:

Total Per file
oxc-transform@0.136.0 19,779 ms 2.88 ms
oxc-transform-react@0.145.0 4,430 ms 0.64 ms

4.46x faster. The native binary also shrinks from 7.3 MB to 4.0 MB (−45%).

Bundle output grows marginally: 55,910.3 kB → 55,946.7 kB raw (+0.07%), 22,004.2 kB → 22,019.0 kB gzipped (+0.07%), from the handful of components 0.145 successfully memoizes that 0.136 bailed on.

Fixed Issues

$
PROPOSAL:

Tests

  1. Run npm install to pick up oxc-transform-react and drop oxc-transform.
  2. Run npm run build and verify it completes with no Oxc transform errors.
  3. Run npm run web, open the app, and verify it loads and renders normally.
  4. Edit any component's JSX while the dev server is running and verify Fast Refresh applies the change without a full reload.
  5. Run npm run react-compiler-compliance-check check src/GlobalModals.tsx and verify OXC reports Cannot access refs during render with no [ReactCompiler] prefix.
  6. Create a new file src/Probe.tsx containing a component that reads ref.current during render, run npm run react-compiler-compliance-check check-changed, and verify it fails with New file fails to compile with the oxc React Compiler.
  7. Replace that file's body with a component that has no Rules-of-React violation, re-run npm run react-compiler-compliance-check check-changed, and verify it passes.
  8. Delete src/Probe.tsx.
  9. Run npm run lint and verify it exits 0.
  • Verify that no errors appear in the JS console

Offline tests

N/A — this changes build tooling only. No runtime code, network behaviour, or Onyx interaction is affected.

QA Steps

N/A — [No QA]. There is no user-facing change; the compiled output is equivalent apart from a handful of components that gain React Compiler memoization.

  • Verify that no errors appear in the JS console

PR Author Checklist

  • I linked the correct issue in the ### Fixed Issues section above
  • I wrote clear testing steps that cover the changes made in this PR
    • I added steps for local testing in the Tests section
    • I added steps for the expected offline behavior in the Offline steps section
    • I added steps for Staging and/or Production testing in the QA steps section
    • I added steps to cover failure scenarios (i.e. verify an input displays the correct error message if the entered data is not correct)
    • I turned off my network connection and tested it while offline to ensure it matches the expected behavior (i.e. verify the default avatar icon is displayed if app is offline)
    • I tested this PR with a High Traffic account against the staging or production API to ensure there are no regressions (e.g. long loading states that impact usability).
  • I included screenshots or videos for tests on all platforms
  • I ran the tests on all platforms & verified they passed on:
    • Android: Native
    • Android: mWeb Chrome
    • iOS: Native
    • iOS: mWeb Safari
    • MacOS: Chrome / Safari
  • I verified there are no console errors (if there's a console error not related to the PR, report it or open an issue for it to be fixed)
  • I followed proper code patterns (see Reviewing the code)
    • I verified that comments were added to code that is not self explanatory
    • I verified that any new or modified comments were clear, correct English, and explained "why" the code was doing something instead of only explaining "what" the code was doing.
    • I verified any copy / text that was added to the app is grammatically correct in English. It adheres to proper capitalization guidelines (note: only the first word of header/labels should be capitalized), and is either coming verbatim from figma or has been approved by marketing (in order to get marketing approval, ask the Bug Zero team member to add the Waiting for copy label to the issue)
  • If a new code pattern is added I verified it was agreed to be used by multiple Expensify engineers
  • I followed the guidelines as stated in the Review Guidelines
  • I tested other components that can be impacted by my changes (i.e. if the PR modifies a shared library or component like Avatar, I verified the components using Avatar are working as expected)
  • If a new CSS style is added I verified that:
    • A similar style doesn't already exist
    • The style can't be created with an existing StyleUtils function (i.e. StyleUtils.getBackgroundAndBorderStyle(theme.componentBG))
  • If new assets were added or existing ones were modified, I verified that:
    • The assets are optimized and compressed (for SVG files, run npm run compress-svg)
    • The assets load correctly across all supported platforms.
  • If the PR modifies code that runs when editing or sending messages, I tested and verified there is no unexpected behavior for all supported markdown - URLs, single line code, code blocks, quotes, headings, bold, strikethrough, and italic.
  • If the PR modifies a generic component, I tested and verified that those changes do not break usages of that component in the rest of the App (i.e. if a shared library or component like Avatar is modified, I verified that Avatar is working as expected in all cases)
  • If the PR modifies a component related to any of the existing Storybook stories, I tested and verified all stories for that component are still working as expected.
  • If the PR modifies a component or page that can be accessed by a direct deeplink, I verified that the code functions as expected when the deeplink is used - from a logged in and logged out account.
  • If the PR modifies the UI (e.g. new buttons, new UI components, changing the padding/spacing/sizing, moving components, etc) or modifies the form input styles:
    • I verified that all the inputs inside a form are aligned with each other.
    • I added Design label and/or tagged @Expensify/design so the design team can review the changes.
  • I added unit tests for any new feature or bug fix in this PR to help automatically prevent regressions in this user flow.
  • If the main branch was merged into this PR after a review, I tested again and verified the outcome was still expected according to the Test steps.

Screenshots/Videos

Android: Native

N/A — build tooling change, web build only.

Android: mWeb Chrome

N/A — build tooling change, web build only.

iOS: Native

N/A — build tooling change, web build only.

iOS: mWeb Safari

N/A — build tooling change, web build only.

MacOS: Chrome / Safari

Local CI parity run on this branch:

Check Result
react-compiler-compliance-check check-changed pass
react-compiler-compliance-check check (synthetic violations) correctly fails; clean file passes
npm run lint pass — 8,388 files, exit 0
npm run typecheck (tsc, CI gate) pass
npm run typecheck-tsgo pass
npm test (jest) pass — 1,205 suites, 21,434 tests
npm run test:bun pass — 341 tests, 26 files
npm run fmt (oxfmt) pass — no diff
npx knip pass — no new issues
scripts/findUnusedStyles.ts pass
scripts/validatePatches.sh pass
npm run build (production web) pass

npm run lint needs ESLINT_CONCURRENCY=4 on a 128 GB host, which scripts/lint.ts already documents; with auto its workers OOM. That is unrelated to this change — a memory probe over 1,500 files through didBothCompilersMemoizeFile shows this branch peaking at 507 MB RSS against 960 MB on main.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant