react-native — Engineering Performance
63 engineers all time · Jan 2025 – Sep 2026 · built 2026-09-30 · GitHub
Performance snapshot
Today's rolling 90-day reading for react-native, compared with the start of the series. Pick a window to move that comparison point.
Avg. perf / dev / mo
+4.0%
0.70 → 0.73 ETV
Active engineers
−36.4%
44.0 → 28.0
Features
−0.7pp
29.4% → 28.7%
vs. Meta
0.37x
0.98x → 0.37x · −63% below
react-native vs. Meta
Per-engineer ETV for react-native against Meta as a whole. Both lines are 90-day rolling averages scaled to a 30-day month, so they share one axis and can be read against each other at any point. Pick a window to zoom the chart to it.
Performance Composition
Each month's output split by type of work: Features (new value), Maintenance (sustaining systems), Tests, Docs, and Fixes (rework). The yellow line is output per engineer, so when it rises each engineer is delivering more, whatever the team size did. Unit: Engineering Throughput Value (ETV).
Engineering capacity
Effective engineers behind react-native, against its pre-AI baseline. Each subject has its own: react-native's is 0.70 ETV / dev / mo, its first reading in Q1 2025. Per-engineer ETV divided by that gives a capacity multiple, and that multiple applied to the engineers active in the trailing 90 days turns it into engineer-equivalents. The line is the real headcount, so the gap between line and area is what the leverage is worth. Because each baseline is its own, every subject opens at 1.0x on its first day: multiples measure improvement and are not comparable between subjects.
Knowledge concentration
How dependent is this repo on a small number of engineers? Higher top-1 share = higher key-person risk.
Rubén Norte owns 9.1 % of commits.
Behind the numbers
Written summary of the work completed each month.
No monthly reports available yet.
Top engineers
Most impactful commits
Top 10 by ETV in the all-time window.
- 8.4ETVfeat(iOS) swiftpm support in iOS (#57332) Summary: ⚠️ This is a stacked PR — review/merge the base PRs first Builds on, and should land after (bottom-up): 1. https://github.com/react/react-native/issues/57285 — Remove the Clang VFS overlay / modularize React headers (base of the stack). <React/…> via the framework module map; lowercase namespaces (react/, yoga/, jsi/, …) via the ReactNativeHeaders module map. 2. https://github.com/react/react-native/issues/57305 — Prebuilt artifact resources + ReactNativeHeaders.xcframework. Ships the prebuilt core artifacts (incl. ReactNativeHeaders in the tarball + embedded React.framework resources) and the CocoaPods React-Core-prebuilt facades. 3. https://github.com/react/react-native/issues/57440 — Self-serving prebuilt ReactNativeDependencies + dependency-only facades. The deps pod becomes the single header authority for the third-party namespaces (RCT-Folly/glog/boost/…); community `s.dependency "RCT-Folly"` resolves to local facades instead of trunk source pods. 4. https://github.com/react/react-native/issues/57442 — ReactNativeDependenciesHeaders sidecar + pure-RN ReactNativeHeaders (immediate base). The deps prebuild emits a headers-only LIBRARY-type sidecar (the binary deps xcframework is framework-type — invisible to SwiftPM binaryTargets), ReactNativeHeaders drops the third-party namespaces, and both headers artifacts publish standalone to Maven. The whole stack is rebased onto current main (2026-07-06). This PR consumes the final **five-artifact set**: React, ReactNativeHeaders (pure-RN), ReactNativeDependencies, ReactNativeDependenciesHeaders, hermes-engine. Adds npx react-native spm and the package-generation tooling that turns an app into a SwiftPM-integrated RN app using the base stack's prebuilt XCFrameworks. CocoaPods stays supported — additive, opt-in, no Ruby toolchain. Integration is injected into the existing .xcodeproj in place (nothing generated/renamed/replaced), recorded in .spm-injected.json so it reverses exactly. ### What this PR adds - The spm CLI (add/update/deinit/scaffold + hidden sync/codegen/download), zero-arg auto-resolution, --deintegrate for CocoaPods→SwiftPM. - SwiftPM package generation (scripts/spm/): autolinking→Package.swift, Codegen→React-GeneratedCode, core XCFramework binary targets, artifact download/cache, surgical pbxproj inject/remove. - ReactNativeDependenciesHeaders wired as the 5th binaryTarget: the headers-only companions (ReactNativeHeaders from the core tarball, the deps sidecar from the deps tarball) are staged automatically out of their parent tarballs — no --headers-tarball priming; REQUIRED_ARTIFACTS covers all five. - Community-library scaffolding from podspecs (incl. root-level-source podspecs via generated include/<SwiftName>/ shims, sibling wiring for transitive spm.dependencies, self-ingestion guards). - In-place Xcode integration (XCLocalSwiftPackageReference + Sync build phase + scheme pre-action + auto-sync; ${PODS_ROOT}-anchored REACT_NATIVE_PATH replaced on --deintegrate with exact deinit rollback). - rn-tester + helloworld SwiftPM consumption + test library; npm-packaging hygiene; a small RNCoreFacades.podspec_dir fix. ## Documentation - **Tool docs** (usage, quick start, architecture): [`scripts/spm/__doc__/spm-scripts.md`](https://github.com/react/react-native/blob/chrfalch/swift-package-manager/packages/react-native/scripts/spm/__doc__/spm-scripts.md) - **Header-paths contract** (how the five artifacts serve headers): [`scripts/spm/__doc__/spm-header-paths-contract.md`](https://github.com/react/react-native/blob/chrfalch/swift-package-manager/packages/react-native/scripts/spm/__doc__/spm-header-paths-contract.md) - **RFC** (updated): [react-native-community/discussions-and-proposals#994](https://github.com/react-native-community/discussions-and-proposals/pull/994) ## Changelog: [IOS] [ADDED] - `npx react-native spm` command + SwiftPM package-generation tooling (opt-in; CocoaPods stays supported) Pull Request resolved: https://github.com/react/react-native/pull/57332 Test Plan: 350 scripts/spm unit tests (incl. byte-identical add→deinit round-trip); manual E2E against the five-artifact set: fresh app via `cli init`→`spm add --deintegrate`→build (artifact resources verified in the app), rn-tester in-place migration (RNTesterPods.xcodeproj, test libraries + spmModules), helloworld in-place migration — all BUILD SUCCEEDED. Verified `npx react-native spm <cmd>` from the command line in each journey. ## Scope & limitations iOS, prebuilt-only (no build-from-source yet); full spm.xcframework/spm.source library metadata not yet (app-local spm.modules + scaffolding are); Expo not yet. ## Follow-ups Remote-mode ReactNative Package.swift must vend the ReactNativeDependenciesHeaders product (spm-distribution repo); library self-containment for repo-portable manifests; build-from-source. Reviewed By: mdvacca Differential Revision: D111449548 Pulled By: cipolleschi fbshipit-source-id: 87a873bf5c8be2d60f7893611e16924bfa730a46Christian Falch · 47fad096 · 2026-07-15
- 3.7ETVMove c++ native animated code to github (#51509) Summary: Pull Request resolved: https://github.com/facebook/react-native/pull/51509 ## Changelog: [Internal] [Added] - Move c++ native animated code to github ⚠️ Note that this is not buildable in OSS yet ⚠️ Reviewed By: sammy-SC, rshest Differential Revision: D75169430 fbshipit-source-id: 499ce209f815474f12007c3c74f81fe0c9ad3f86Zeya Peng · cb02744f · 2025-05-22
- 3.5ETVDerive an SPM library's Swift name from its podspec (#58290) Summary: An autolinked library's SwiftPM target name is also its header import prefix, so deriving it from the npm package name was wrong for most of the ecosystem (`react-native-svg` publishes `RNSVG`, not `ReactNativeSvg`) and wrong silently — no error, just headers nobody can import under the expected name. ## How: This PR reads the podspec file on scaffolding, and will use the name from the podspec if available. In addition it deprecates the react-native.config.js `spm` section in favor of the library's `package.json` file. ### Configuration A package's SwiftPM settings now live in `swiftpmConfig` in its `package.json`, following `codegenConfig`'s conventions: `name`, `dependencies`, `autolinkingPlugin` and `scaffold` for a library, `modules` and `denyPlugins` for an app. The `spm` block in react-native.config.js is deprecated — still read, so nothing breaks, but it warns once per file and package.json wins field by field. ### Resolving A name resolves from `swiftpmConfig.name`, then the deprecated `spm.name`, then the podspec's `header_dir` or name, then the npm name. The podspec is thereby transitional rather than permanent: `spm scaffold` records the name it derived as `swiftpmConfig.name` in the library's package.json, so the next run needs no podspec to name it. It never overwrites a name the library already declares, never records a name guessed from the npm name, and reports what it did. ### Failsafety A prefix Swift cannot spell is normalized to the identifier SwiftPM would compile it as, with a warning. A reserved name or two deps landing on one name is a hard error naming swiftpmConfig.name as the fix; the scope-borrowing that auto-corrected collisions is removed, since a name the build invents is a name no #import can predict. Collision checks key on SwiftPM's c99 name, so react-native-svg and react_native_svg no longer pass and then compile as one module. ### Implementation Name resolution reads the two podspec fields it needs with the regex parser, so it adds no `pod ipc spec` spawn, and the full read is memoized on the resolved path so one run reads a podspec once across name resolution, header search paths and scaffolding. `rn-tester` and the Apple test library move to the new location. ## Changelog: [IOS] [FIXED] - Read SwiftPM name from podspec and store in package.json when scaffolding Pull Request resolved: https://github.com/react/react-native/pull/58290 Test Plan: ✅ Unit tests Reviewed By: cortinico Differential Revision: D119159904 Pulled By: cipolleschi fbshipit-source-id: 59a7c62ecb601498060a5721f362dae8c5cc92deChristian Falch · 1cfc5f29 · 2026-09-09
- 3.3ETVGenerate nested scroll view (#55239) Summary: Pull Request resolved: https://github.com/facebook/react-native/pull/55239 This diff adds the ability to experiment with using `NestedScrollView` instead of `ScrollView` as the parent class for `ReactScrollView` on Android. Since Java doesn't support multiple inheritance or conditional parent class selection, this is implemented using code generation: - A Node script (`generate-nested-scroll-view.js`) generates `ReactNestedScrollView.java` and `ReactNestedScrollViewManager.kt` from their respective source files - The generated files are identical to the originals except they extend `NestedScrollView` instead of `ScrollView` - A Buck genrule verifies the generated files stay in sync with source files at build time - The `useNestedScrollViewAndroid` feature flag controls which implementation is used at runtime This approach allows us to safely A/B test the NestedScrollView implementation without requiring JS changes, since both managers register with the same `REACT_CLASS` name (`"RCTScrollView"`). Changelog: [Internal] Reviewed By: alanleedev, javache Differential Revision: D90902079 fbshipit-source-id: 96ccd25b9a217b8e0cd4e3b542b9dda67047cde3Peter Abbondanzo · 5e93036a · 2026-01-21
- 2.2ETVDelete fixTextClippingAndroid15useBoundsForWidth() (#56282) Summary: Pull Request resolved: https://github.com/facebook/react-native/pull/56282 Fixes https://github.com/facebook/react-native/issues/56185 Fixes https://github.com/facebook/react-native/issues/56186 Fixes https://github.com/facebook/react-native/issues/56187 Delete the `fixTextClippingAndroid15useBoundsForWidth()` feature flag and all code gated behind it. This flag was never shipped, and the new code path has been implicated in several bugs. The change removes: - The feature flag definition and all generated plumbing (C++, JNI, Kotlin, JS) - The visual-bounds-based width measurement path in `TextLayoutManager.kt` - The `setUseBoundsForWidth(true)` call in `buildLayout()` - The MobileConfig parameter (`react_fabric.fix_text_clipping_android_15_use_bounds_for_width`) - The Facebook Android and OSS Experimental overrides The old advance-based width logic is retained as the sole code path. Changelog: [Internal] Reviewed By: cortinico Differential Revision: D98427218 fbshipit-source-id: 7c322353d9c0a0548386a486ecd4280b73a66872Nick Gerleman · 5964a197 · 2026-03-31
- 2.0ETVDowngrade offscreen Fabric image requests to prefetch (#56976) Summary: Pull Request resolved: https://github.com/facebook/react-native/pull/56976 changelog: [internal] Use Fabric layout data to classify image shadow nodes as visible or offscreen on Apple platforms. Offscreen image requests now use `ImageRequestPriority::Prefetch`, which is bridged to the existing `RCTImageLoaderPriorityPrefetch` API, while visible images stay at `Immediate`. The new React Native feature flag defaults to `false` until app-specific gating wires it up. Reviewed By: javache Differential Revision: D106074485 fbshipit-source-id: 80ece75cf0d639be1fdbfdd5f19b838b7b64aba6Samuel Susla · 5fbcc6bf · 2026-05-27
- 1.7ETVFix jagged edge rendering on API 28 (#54525) Summary: Pull Request resolved: https://github.com/facebook/react-native/pull/54525 Older versions of Android's clipPath APIs do not properly support antialiasing and BackgroundStyleApplicator relies on this API to clip to border radii that are smaller than provided images or views. As a result, this often leaves jagged edges on the outer edges of these images. Android even calls this out in their documentation under the "Canvas.clipPath" section of https://developer.android.com/topic/performance/vitals/render#common-jank. This change introduces a new API in BackgroundStyleApplicator called `clipToPaddingBoxWithAntiAliasing` which takes an additional argument over the existing `clipToPaddingBox`: a draw call. If non-null and the Android API is 28 or lower, a new drawPath operation is used to mark the pixels outside of the drawn contents as transparent. Changelog: [Android][Fixed] - Fixed antialiasing issues from border radius usage in API <= 28 Reviewed By: haixia-meta, lenaic Differential Revision: D86263195 fbshipit-source-id: 5dba744da08a3c9297f921baf7d4f1a354404f74Peter Abbondanzo · e08abbcb · 2025-11-14
- 1.6ETVRN: Flowify `packages/react-native` Mocks & Tests (#51794) Summary: Pull Request resolved: https://github.com/facebook/react-native/pull/51794 Adds `flow` to the remaining mocks and tests that are lacking it in the `packages/react-native` directory. This required adding numerous type annotations and suppressions. Changelog: [Internal] Reviewed By: javache Differential Revision: D75930095 fbshipit-source-id: ebb1280adc656680e00fd579701e31521826b55cTim Yung · 5ce99e7e · 2025-06-04
- 1.6ETVFormalize Text in JS as overflow: hidden by default (fixes overflow: visible text with props 2.0) (#55553) Summary: Pull Request resolved: https://github.com/facebook/react-native/pull/55553 RN components on the native side currently act like a mix of both overflow: hidden and overflow: visible, veering closer to the former in the ways that matter most. PreparedLayoutTextView lets us implement this more correctly, and either clip text to padding box, or not clip the text at all. For compat, PreparedLayoutTextView/PreparedLayoutTextViewManager defaulted to `overflow: hidden`. Props 2.0, broke this, since we need to see overflow state change in Fabric props, which defaults to Yoga blank style defaults (always `overflow: visible`). The sanest solution I could find was to change Android native view to keep same defaults as Fabric style layer, then explicitly set `overflow: hidden` at the style layer, as the default behavior. This does slightly change behavior, so that components which previously clipped at the bounding rect of their border box, now clip at their padding box. I.e. text will not be drawn over borders, or rounded corners, by default after this change. Changelog: [General][Changed] - Text is no longer drawn on top of borders by default [General][Changed] - Text outside the bounds of a borderRadius is now hidden by default Reviewed By: mdvacca Differential Revision: D93324626 fbshipit-source-id: 955975f01ca0333dd135e236f26f5dab828a7491Nick Gerleman · 2111bec8 · 2026-02-20
- 1.6ETVfeat(swiftpm): let autolinking plugins declare build-time script phases (#57757) Summary: **SwiftPM has no equivalent of CocoaPods' `script_phase`, so a framework that generates content at build time can't get one.** The first casualty is expo-constants: nothing writes `EXConstants.bundle/app.config`, which shows up at runtime as *"Unable to find the embedded app config"*. This adds a 6th field to the SwiftPM autolinking plugin contract: ```js scriptPhases: [{ id: 'expo-constants.app-config', // stable: ledger key + deterministic UUID seed name: 'Generate Expo app.config', // Xcode's display name script: '…', position: 'end', // 'end' (default) | 'beforeCompile' inputPaths: ['$(SRCROOT)/../app.json'], outputPaths: ['$(TARGET_BUILD_DIR)/…/EXConstants.bundle/app.config'], alwaysOutOfDate: true, }] ``` The plugin returns data; RN validates it, records it to a `.spm-plugin-script-phases.json` sidecar (written even when empty, so removing a plugin clears stale entries), and `spm add`/`update` emits one `PBXShellScriptBuildPhase` per entry — tracked in `.spm-injected.json` by `id`, so `update` reconciles and `deinit` reverts. ### Verified end to end by the Expo team On a real Expo app, against a local cut of this branch. A `position: 'end'` phase lands last, after the JS bundle phase: ``` 5. Resources 6. Bundle React Native code and images 7. [Expo Dev Launcher] Strip Local Network Keys for Release 8. Generate Expo app.config ← last ``` `BUILD SUCCEEDED`, and `EXConstants.bundle/app.config` is written with `sdkVersion: 56.0.0` — precisely the value whose absence caused the original bug. Red baseline confirmed first: before the declaration, the same app built with `0 script phase(s)`, an empty sidecar, and no `EXConstants.bundle` at all. They also independently confirmed `deinit` leaves zero residue, `add` is idempotent (same sha1 twice), and no phase duplicates. Their side is expo/expo#47647. ### Design decisions worth a reviewer's attention - **`end` appends at the true end of `buildPhases`**, which is *after* the JS bundle phase — where expo-constants must write, since it targets `$TARGET_BUILD_DIR`. Anchoring relative to the Frameworks phase (the obvious-looking choice) lands it *before* the bundle phase, because real template order is `Sources, Frameworks, Resources, Bundle React Native code and images`. - **`beforeCompile` anchors after RN's own "Sync SPM Autolinking" phase**, which must stay first since it regenerates autolinking — a plugin phase ahead of it would run against stale generated content. The anchor chains forward so declared order survives. Position and relative order are re-derived every sync, so a phase dragged by hand in Xcode returns to its declared slot. - **Validation is fatal**, matching `flavoredFrameworks` rather than the lenient `watchPaths`. A silently dropped phase means the content is never written and the app fails at runtime with no build-time signal — which is the bug being fixed. - **`id` is the ledger key and the UUID seed.** The charset allows a scoped npm name (`expo/log-box`) but excludes `:`, which separates the `plugin:<id>` seed. `__proto__`/`constructor`/`prototype` are rejected because `plainObject['__proto__'] = v` vanishes through `JSON.stringify`, which would record a phase that `deinit` could never remove. - **A plugin-supplied `name` reaches pbxproj comments**, and those are scanned by single-line regexes. What lands in a comment is therefore normalized: a name containing `*/`, `{` or `,` otherwise produced a brace-unbalanced project Xcode couldn't open, or an orphan phase `deinit` reported removing but didn't. The full name still goes verbatim into the `name` field Xcode displays. The same normalization now covers generated-source filenames, which had the identical hole. ### Also fixed in passing `add → update → deinit` did **not** restore `project.pbxproj` byte-for-byte, even with zero script phases: the second run's marker forgot what the first had created, leaving an empty `packageReferences` / `packageProductDependencies` and the generated `.xcscheme` behind. The created-record now carries forward and `scheme.created` is sticky. Two guards came with that, both tested: a created array field is removed only when it is **empty** after RN's own members come out (so a package a user added to it survives `deinit`), and the scheme is deleted only if it is still RN's own (so a scheme the user has taken over is left alone). ## Changelog: [Internal] [Added] - SwiftPM: autolinking plugins can declare build-time script phases via `scriptPhases` Pull Request resolved: https://github.com/react/react-native/pull/57757 Test Plan: `yarn jest packages/react-native/scripts` → **853 tests**, all green. The SwiftPM suite specifically went from **462 → 637** tests. Written red first throughout. Coverage includes: - one declared phase → exactly one `PBXShellScriptBuildPhase`, correct `name`/`shellScript`/serialized paths; `alwaysOutOfDate` emitted as unquoted `1` only when set - `end` lands last; `beforeCompile` lands after the sync phase and before Sources; declared order preserved for multiple phases of each position and for a mix; a changed `position` is re-seated on the next sync - add / update-in-place / remove keyed on `id`; unchanged re-sync byte-identical; `deinit` byte-identical with no orphan object or section - a 17-row hostile-`name` matrix (`{ } ( ) , ; = */ /* * /`, unbalanced quote, tab, unicode, 300 chars, a name that normalizes to empty) × {balanced after add, byte-identical re-inject, clean deinit} - scripts containing quotes, backslashes, newlines and `$(VAR)` round-trip through emission, refresh and deinit - contract validation: 16 malformed-entry cases, duplicate `id` across plugins, reserved ids, scoped ids accepted, `:` rejected - `add → update → deinit` byte-identity with zero phases and with two **Not covered by unit tests, deliberately:** that `end` runs after the JS bundle phase. The `plain-app.pbxproj` fixture has no bundle phase, so it is unassertable here — this is disclosed in a comment at the test rather than papered over, and is exactly what the Expo verification above establishes. **Known limitation, not addressed here:** against a project Xcode has previously saved, `add → deinit → add` is structurally identical (same UUIDs, same reference counts) but not byte-identical — Xcode writes multi-line dicts in sorted order, the injector writes single-line dicts in insertion order, which shows up as formatting churn in a committed `project.pbxproj`. Pre-existing for every object the injector emits, not specific to script phases, and filed separately. ## Note on landing order This touches `generate-spm-xcodeproj.js` and `spm-pbxproj.js`, which https://github.com/react/react-native/issues/57744 and https://github.com/react/react-native/issues/57756 also touch — including the same marker-write block. All three are cut independently from `main`; whichever lands first, I'll rebase the others. Happy to restack in whatever order is easiest to review. Reviewed By: fabriziocucci Differential Revision: D114318236 Pulled By: cipolleschi fbshipit-source-id: 4aa7958c302323299eda9db17adcec0d86edeebeChristian Falch · 6ff47ef9 · 2026-08-10