cherry-pick(4.3-stable): precompiled headers on Android (#9434, #9455)#9739
Merged
MatiPl01 merged 2 commits intoJul 2, 2026
Merged
Conversation
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
piaskowyk
approved these changes
Jun 26, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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 tobuild.gradle.kts), so theapply from: "./generate-stub-pch.gradle.kts"line was added to the Groovy build files instead of the.ktsones. Thegenerate-stub-pch.gradle.ktsscript itself is unchanged and applied cross-DSL (Gradle 9.3 supports applying a Kotlin script plugin from a Groovy build). The CMaketarget_precompile_headerschanges and the*PCH.hfiles applied as-is.Not build-verified locally; relying on CI for the Android build and lint jobs.