Skip to content

cherry-pick(4.3-stable): precompiled headers on Android (#9434, #9455)#9739

Merged
MatiPl01 merged 2 commits into
4.3-stablefrom
@matipl01/4.3-stable/cherry-pick-#9434-#9455
Jul 2, 2026
Merged

cherry-pick(4.3-stable): precompiled headers on Android (#9434, #9455)#9739
MatiPl01 merged 2 commits into
4.3-stablefrom
@matipl01/4.3-stable/cherry-pick-#9434-#9455

Conversation

@MatiPl01

Copy link
Copy Markdown
Member

Summary

Cherry-picking for Reanimated 4.3.2 release. #9455 is the clang-tidy CI fix for the PCH setup introduced in #9434, so both are included in this single PR (cherry-picked in order).

Cherry-pick notes

4.3-stable still uses the Groovy build.gradle (main migrated to build.gradle.kts), so the apply from: "./generate-stub-pch.gradle.kts" line was added to the Groovy build files instead of the .kts ones. The generate-stub-pch.gradle.kts script itself is unchanged and applied cross-DSL (Gradle 9.3 supports applying a Kotlin script plugin from a Groovy build). The CMake target_precompile_headers changes and the *PCH.h files applied as-is.

Not build-verified locally; relying on CI for the Android build and lint jobs.

tomekzaw and others added 2 commits June 23, 2026 14:00
This PR adds precompiled headers (PCH) to the Android CMake builds for
`libreanimated.so` and `libworklets.so`. PCH lets clang parse the heavy
stable-third-party headers (`folly/dynamic.h`, `jsi/jsi.h`, `fbjni.h`,
RN renderer headers, C++ standard library) once per build instead of
once per translation unit.

This is a build-time-only change. No runtime behavior, ABI, or `.so`
size is affected.

Inspired by [expo/expo#39641](expo/expo#39641)
by @lukmccall from Expo SDK team – huge thanks for your help and
insights!

Eight files. The PCH itself is four files; both libs follow the same
pattern:

- `packages/react-native-reanimated/android/CMakeLists.txt` — bump
`cmake_minimum_required` to `3.16` (needed for
`target_precompile_headers`); add `target_precompile_headers(reanimated
PRIVATE ${ANDROID_CPP_DIR}/ReanimatedPCH.h)`.
-
`packages/react-native-reanimated/android/src/main/cpp/ReanimatedPCH.h`
— new file. stdlib subset + `folly/dynamic`, `folly/json/dynamic`,
`folly/Format`, `folly/Conv`, `folly/Expected` + `jsi/jsi`,
`jsi/JSIDynamic` + `fbjni/fbjni` + 8 RN renderer headers (`ShadowNode`,
`ShadowTree`, `UIManager`, `ViewProps`, `BaseViewProps`, `RootProps`,
`RootShadowNode`, `EventDispatcher`).
- `packages/react-native-worklets/android/CMakeLists.txt` — same shape
(`cmake_minimum_required` → `3.16`, `target_precompile_headers(worklets
PRIVATE ...)`).
- `packages/react-native-worklets/android/src/main/cpp/WorkletsPCH.h` —
new file. stdlib subset + `jsi/jsi` + `fbjni/fbjni`. Smaller than
reanimated's set because worklets has fewer TUs (36 vs 96) and adding
more was net-negative.

Four more files implement the [Android Studio sync
workaround](#android-studio-sync-workaround):

- `packages/react-native-reanimated/android/build.gradle.kts`,
`packages/react-native-worklets/android/build.gradle.kts` — apply
`./generate-stub-pch.gradle.kts`.
-
`packages/react-native-reanimated/android/generate-stub-pch.gradle.kts`,
`packages/react-native-worklets/android/generate-stub-pch.gradle.kts` —
new gradle scripts.

No internal Reanimated/Worklets headers are PCH'd, so local edits to
project sources never invalidate the PCH and trigger a full rebuild.

No `-Wpedantic`/`-Werror` suppression pragmas were needed — both PCHs
compile clean. clangd's `misc-include-cleaner` warnings on the stdlib
includes are silenced via `// IWYU pragma: begin_keep / end_keep`.

`react/renderer/animationbackend/AnimationBackend.h` was deliberately
excluded from the PCH because it does not exist on some older RN
versions which are still supported per `peerDependencies`.

Each variant measured 5 cold runs, median, on M3 Pro (12 cores),
arm64-v8a only.

- **C++ compile CPU**: pure `ninja` invocation (CMake configure done
once up-front; `.o` / `.pch` / `.so` wiped between runs); `time -p
ninja` reports user+sys CPU summed across cores. Machine-independent.
- **Native-only wall**: same `ninja` invocation, real time.
- **End-to-end wall**: `./gradlew :app:assembleDebug
-PreactNativeArchitectures=arm64-v8a --no-build-cache
--no-configuration-cache --no-daemon`; full wipe of every `build/` and
`.cxx/` between runs.

Header set was picked by ranking external/stable headers from `clang
-ftime-trace` by `sum(parse_time) × TU_count` and keeping only those
reaching ~60% TU coverage on reanimated (≥85% on worklets, since its
smaller TU pool tolerates less PCH overhead). Several wider/narrower
variants were measured.

| Frame | Baseline | PCH | Δ |
|---|---|---|---|
| C++ compile work (CPU, both libs, per ABI) — what PCH actually does |
242.4s | 117.1s | **−51.7%** |
| Native-only wall (`ninja`, both libs combined) | 25.2s | 15.9s |
**−37%** |
| End-to-end `:app:assembleDebug` wall (arm64-v8a, no caches) | 74.2s |
64.9s | **−12.5%** (−9.25s) |

The end-to-end −12.5% is the conservative number — that's the wall-clock
gain a developer running a single-ABI debug build on this machine sees.
The native-only and CPU numbers are the headline "this is what the
change is doing" metrics. On CI runners with fewer cores or when
building 4 ABIs, the wall-clock improvement scales much closer to the
CPU improvement.

Per-lib breakdown of the pure-`ninja` measurement:

| Lib | TUs | Baseline CPU | PCH CPU | Δ CPU | Baseline wall | PCH wall
| Δ wall |
|---|---|---|---|---|---|---|---|
| react-native-worklets | 36 | 35.40s | 23.18s | **−34.5%** | 4.06s |
3.57s | −12.1% |
| react-native-reanimated | 96 | 207.00s | 93.87s | **−54.7%** | 21.13s
| 12.30s | −41.8% |

`target_precompile_headers` causes CMake to generate `cmake_pch.hxx` (a
wrapper that `#include`s every header listed in `*PCH.h`), build it once
into a `cmake_pch.hxx.pch` binary, and force-include the wrapper via
`-Xclang -include -Xclang …cmake_pch.hxx` in every TU compile command
(clang transparently loads the paired `.pch`). clang emits `.d`
dependency files for both the PCH compile and each TU; ninja stores
these and stats every recorded path on each build. If anything
transitively pulled in by the PCH changed mtime, the `.pch` is rebuilt —
and because every TU depends on the `.pch`, they all rebuild too.

| Scenario | What rebuilds | Cost vs no-PCH |
|---|---|---|
| Edit a Reanimated/Worklets `.cpp` | Just that TU | Same |
| Edit a Reanimated/Worklets `.h` not in PCH | Every TU that includes it
| Same |
| Edit `*PCH.h` itself | PCH binary + every TU in that lib | Same as a
clean lib build |
| RN patch bump (same prefab transform hash) | Usually nothing | Same |
| RN minor bump | PCH binary + every TU | Same as today (RN bumps
already invalidate every TU because prefab include paths are part of
compile commands) |
| NDK / libc++ update | PCH binary + every TU | Same as today |
| Compile-flag change (feature flag macro, build type) | PCH binary +
every TU | Same as today |

PCH never adds invalidation — it only adds an extra build artifact that
recompiles in lock-step with the things that already trigger a full
rebuild. The invariant is: any change that previously forced a clean lib
rebuild still does, plus a ~1–2 s PCH compile, then every TU is faster.

clang refuses to load a PCH built with mismatched flags (different
language standard, target, defines, etc.) — it errors `module file built
with different flags` rather than silently producing wrong code. So a
stale `.pch` is a build failure, not a runtime bug.

The PCH binary lives inside
`.cxx/<config>/<hash>/<abi>/CMakeFiles/<target>.dir/` and is wiped by
`gradlew clean` and the existing `cleanCMakeCache` task — no new
maintenance.

One incremental-dev gotcha worth flagging for future contributors:
adding a `#include` to a `*PCH.h` to "speed up my TU" forces every
contributor's next build to rebuild the entire lib.

Enabling PCH causes Android Studio's Gradle Sync to surface a C++
indexer error on a clean checkout — the IDE can't index sources without
the precompiled header binary (`cmake_pch.hxx.pch`), which doesn't exist
until the first build. The project compiles fine; the failure is
sync-only.

<img width="384" height="110"
src="https://github.com/user-attachments/assets/284623f6-823f-4edb-8337-447d594bd4f4"
/>

To unblock sync **without making sync slower**, both packages apply
`generate-stub-pch.gradle.kts`. After `configureCMakeDebug`, it walks
`compile_commands.json` and compiles a **stub `.pch` from an empty
header** for every ABI. Generating the real PCH here would defeat the
purpose — it'd push several seconds of full PCH compile into every sync.
The stub satisfies the indexer, and its mtime is backdated below
`cmake_pch.hxx.cxx` so the next real build invokes ninja to regenerate
the proper PCH binary in the normal build pipeline.

Adapted from [expo/expo#45921](expo/expo#45921).

- [ ] `yarn android` from `apps/fabric-example` — app builds and
launches; existing screens behave normally.
- [ ] CI: existing Android build + lint jobs pass.
- [ ] Spot-check on RN 0.81 / 0.82 / 0.83 / 0.84 / 0.85 to confirm none
of the PCH'd RN renderer headers regress on older versions.
- [ ] Open `apps/fabric-example/android` in Android Studio on a clean
checkout (no prior `.cxx/` build artifacts), run Gradle Sync, confirm no
C++ indexer error like the one in the workaround section appears and
that `./gradlew :app:generateStubPCH` produced stub `.pch` files under
`.cxx/`.
- [ ] After the sync, run a full debug build and verify ninja
regenerates the real PCH binaries (i.e. stub mtimes get overwritten and
the proper PCH files are produced).
## Summary
We use precompiled headers in android build
This breaks clang-tidy CI when clang version is mismatched between NDK
and ubuntu/system clang
This PR makes the clang-tidy script use the NDK clang
## Test plan
CI works
@MatiPl01 MatiPl01 merged commit d146192 into 4.3-stable Jul 2, 2026
14 checks passed
@MatiPl01 MatiPl01 deleted the @matipl01/4.3-stable/cherry-pick-#9434-#9455 branch July 2, 2026 15:31
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.

4 participants