Repository navigation
Conversation
iOS Release builds compiled React Native's C, C++ and Objective-C code at Xcode's default -Os, while Android builds the same code at -O2. Set GCC_OPTIMIZATION_LEVEL=2 for Release in the React.xcframework and ReactNativeDependencies.xcframework prebuilds and for React Native pods in CocoaPods source builds. Yoga and glog get -O2 in every configuration, as on Android. Hermes keeps MinSizeRel.
ReactCodegen and ReactAppDependencyProvider are compiled from the SwiftPM codegen template. CocoaPods already builds the same generated pods at -O2 in Release; this gives SwiftPM apps the same optimization level.
chrfalch
force-pushed
the
chrfalch/ios-cxx-optimization-level
branch
from
October 20, 2026 13:49
7812049 to
ed6c266
Compare
Contributor
|
Amazing work! Congrats! 👏 |
cipolleschi
reviewed
Oct 20, 2026
cipolleschi
left a comment
Contributor
There was a problem hiding this comment.
Thanks for working on this.
I left a comment for Cocoapods.
chrfalch
marked this pull request as ready for review
October 20, 2026 15:40
cipolleschi
approved these changes
Oct 20, 2026
|
@cipolleschi has imported this pull request. If you are a Meta employee, you can view this in D126452701. |
Collaborator
|
This pull request was successfully merged by @chrfalch in 60c817e When will my fix make it into a release? | How to file a pick request? |
|
@cipolleschi merged this pull request in 60c817e. |
cipolleschi
pushed a commit
that referenced
this pull request
Oct 26, 2026
Summary:
iOS Release builds compile React Native's C, C++ and Objective-C code with `-Os` (optimize for size). React Native never sets `GCC_OPTIMIZATION_LEVEL`, so Xcode's default applies. Android compiles the same code with `-O2` (optimize for speed) in Release. Yoga even sets `-O2` explicitly ("prioritize speed", `ReactCommon/yoga/cmake/project-defaults.cmake`).
This PR sets `GCC_OPTIMIZATION_LEVEL = 2` for Release builds of React Native's own code on iOS, to match Android. Android also builds Yoga and glog at `-O2` in Debug builds, so this PR does the same on iOS. All other code keeps `-O0` in Debug. Hermes keeps its size-optimized `MinSizeRel` build, as on Android. Swift code is not affected.
In a layout benchmark on Apple Silicon, a Yoga layout takes 21–54% less time at `-O2` than at `-Os` in Release. In Debug it takes about 94% less time at `-O2` than at `-O0` (16–19 times faster). The stripped React binary grows by 6.2%.
After this PR, every library that both platforms compile uses the same optimization level: React Native core, Yoga, folly, glog, fmt, boost and double-conversion. Two differences stay out of scope: Android also uses link-time optimization, and Android turns off Yoga's assertions in Debug builds.
I found no commit that chose `-Os` for iOS on purpose. The earlier optimization decisions were all for Android: `c04ab921924` (2016, "Use -O3 to compile CSSLayout in open source … This is how we compile internally") and #37371 (2023, Yoga's reference CMake build at `-O2`).
The question came up in the community. A user profiling Bluesky found Yoga about 20% faster at `-O2` and layout 2–5% faster in the full app: bluesky-social/social-app#12061. Their app-level workaround is expo/expo#51770. An app-level setting cannot reach the prebuilt React Native frameworks, so the fix belongs here.
### What changes
| Build path | Change |
|---|---|
| Prebuilt `React.xcframework` (`packages/react-native/scripts/ios-prebuild/build.js`) | Adds `GCC_OPTIMIZATION_LEVEL=2` to the `xcodebuild` command for the Release flavor only. |
| Prebuilt `ReactNativeDependencies.xcframework` (`scripts/releases/ios-prebuild/build.js`) | Same, for folly, glog, fmt, boost, double-conversion, fast_float and SocketRocket. |
| CI cache (`.github/workflows/prebuild-ios-dependencies.yml`) | The slice and xcframework cache keys now include `scripts/releases/ios-prebuild/build.js`. Without this, CI reuses the cached `-Os` build. |
| Yoga in `React.xcframework` (`packages/react-native/Package.swift`) | The `yoga` target gets `-O2` in every configuration, as on Android (`ReactCommon/yoga/CMakeLists.txt`). |
| glog in `ReactNativeDependencies.xcframework` (`scripts/releases/ios-prebuild/configuration.js`) | glog gets `-O2` in every configuration, as on Android (`ReactAndroid/src/main/jni/third-party/glog/CMakeLists.txt`). |
| CocoaPods builds from source (`react_native_post_install`) | New step `set_optimization_level_for_react_native_pods`. It sets level 2 on Release-type configurations of React Native pods, and on every configuration of `Yoga` and `glog`. |
| SwiftPM codegen package (`packages/react-native/scripts/codegen/templates/Package.swift.spm-template`) | `ReactCodegen` and `ReactAppDependencyProvider` get `-O2` in Release through `unsafeFlags`, as the CocoaPods generated pods do. SwiftPM allows `unsafeFlags` here because the app always uses this package from a local path. |
The command-line setting applies to every configuration, so the prebuild scripts add it only when they build Release. The per-target `-O2` for Yoga and glog comes after the configuration's `-O0` on the compiler command line, so it wins in Debug.
In CocoaPods, a React Native pod is one of these:
- a local pod inside the `react-native` package
- one of React Native's third-party dependency pods (`RCT-Folly`, `glog`, `boost`, `DoubleConversion`, `fmt`, `fast_float`, `SocketRocket`)
- one of the generated pods `ReactCodegen` and `ReactAppDependencyProvider`
Community library pods, the app's own pods and `hermes-engine` do not change. Release-type configurations are found by `config.type == :release`, as `add_ndebug_flag_to_pods_in_release` already does, so custom configurations such as "Staging" are included.
An app that wants the old behaviour can set `GCC_OPTIMIZATION_LEVEL` back to `s` in its own `post_install` after `react_native_post_install`.
### Benchmark: layout speed
Yoga built twice from this branch with Apple clang 21.0.0 (`-std=c++20 -DNDEBUG -arch arm64`); only the optimization level differs. The benchmark harness is the same object file in both builds. Every iteration forces a full relayout (root width alternates 390/391). There were 10 runs of each build, alternating. Both builds produce identical layout results.
Machine: Apple M4 Max, macOS 27.0.1. This is a Mac, not an iPhone, and the trees are synthetic.
| Scenario | `-Os` (µs per layout, median) | `-O2` (µs per layout, median) | Time reduction |
|---|---|---|---|
| Feed: 500 rows, each with an avatar, 3 measured text nodes and 4 buttons | 2,879 | 2,261 | 21% |
| Deep: 15 levels, mixed row/column, padding, margin, percent widths | 1,098 | 805 | 27% |
| Grid: 1,000 wrapped items with `aspectRatio` | 1,018 | 472 | 54% |
"Time reduction" = 1 − median(`-O2`) / median(`-Os`). The spread between the fastest and slowest of the 10 runs was 3–11%.
Debug builds, same harness, built without `NDEBUG` so Yoga's assertions stay on, as in an iOS Debug build. Median of 3 alternating runs:
| Scenario | `-O0` (µs per layout) | `-O2` (µs per layout) | Time reduction |
|---|---|---|---|
| Feed | 36,350 | 2,310 | 94% (15.7× faster) |
| Deep | 15,184 | 816 | 95% (18.6× faster) |
| Grid | 8,190 | 486 | 94% (16.9× faster) |
### Benchmark: binary size
Release, `ios-arm64` device slice. Sizes are uncompressed; App Store download size will differ.
| Binary | `-Os` (bytes) | `-O2` (bytes) | Difference |
|---|---|---|---|
| `React.framework/React`, stripped (`strip -x`) | 6,154,912 | 6,537,576 | +382,664 (+6.2%) |
| `React.framework/React`, `__TEXT` segment | 4,931,584 | 5,324,800 | +393,216 (+8.0%) |
| `ReactNativeDependencies.framework`, stripped | 778,240 | 873,296 | +95,056 (+12.2%) |
| Yoga only, `__text` section | 77,140 | 112,188 | +35,048 (+45%) |
The unstripped React binary gets *smaller* (10,785,672 → 10,335,408 bytes). More inlining at `-O2` leaves about 4,950 fewer symbols, so the link-edit data shrinks more than the code grows. The stripped size is the relevant number.
If only Yoga were moved to `-O2`, the stripped React binary would grow by 32,776 bytes (+0.53%).
## Changelog:
[IOS] [CHANGED] - Compile React Native's C/C++/Objective-C code with `-O2` instead of `-Os` in Release, and Yoga and glog with `-O2` in all builds, to match Android
Pull Request resolved: #59226
Test Plan:
**Unit tests.** New Jest tests check that the Release command has `GCC_OPTIMIZATION_LEVEL=2`, the Debug command does not, and the rest of each command is unchanged. They failed before the change and pass after it.
```
yarn jest packages/react-native/scripts/ios-prebuild/__tests__/build-test.js scripts/releases/ios-prebuild/__tests__/build-test.js
Tests: 8 passed, 8 total
```
A new Jest test checks that the SwiftPM codegen template gives `ReactCodegen` and `ReactAppDependencyProvider` the Release-only `-O2` flag in both `cSettings` and `cxxSettings`, and that `ReactAppHeaders` does not get it. It failed before the change and passes after it.
```
yarn jest packages/react-native/scripts/spm/__tests__/codegen-template-optimization-test.js
Tests: 5 passed, 5 total
```
All tests in `packages/react-native/scripts/ios-prebuild` and `scripts/releases`: 24 suites, 196 tests passed. All tests in `packages/react-native/scripts/spm`: 23 suites, 1060 tests passed. ESLint, Prettier and Flow report no errors on the changed files.
**Prebuilt frameworks.** I built `React.xcframework` (`node scripts/ios-prebuild --setup --build --compose --platforms ios --flavor <Debug|Release>`) and `ReactNativeDependencies.xcframework` (`node scripts/releases/prepare-ios-prebuilds.js --setup --build --compose --platforms ios --configuration <Debug|Release>`), then read the last `-O` flag in each target's compiler argument file:
| Build | React Native code | Yoga | glog | Other dependencies |
|---|---|---|---|---|
| Release, before this PR | `-Os` | `-Os` | `-Os` | `-Os` |
| Release, with this PR | `-O2` | `-O2` | `-O2` | `-O2` |
| Debug, with this PR | `-O0` | `-O2` | `-O2` | `-O0` |
**CocoaPods.** `RCT_USE_PREBUILT_RNCORE=0 RCT_USE_RN_DEP=0 bundle exec pod install` in `packages/rn-tester`, then read `Pods.xcodeproj`:
- `Yoga` and `glog`: `GCC_OPTIMIZATION_LEVEL = 2` in Debug and Release.
- Other React Native targets (`React-*`, `RCT-Folly`, `ReactCodegen`, `SocketRocket`, …): 2 in Release, unset in Debug.
- RNTester's own pods (`ScreenshotManager`, `MyNativeView`, `NativeCxxModuleExample`, …) and the app targets: unset in both configurations.
**SwiftPM.** In `private/helloworld`: `node ../../packages/react-native/cli.js spm add --deintegrate --version nightly`, then `xcodebuild -configuration Release -sdk iphonesimulator build`. The build succeeded, and Xcode accepted `unsafeFlags` in the generated package. The `-O` flags in each target's compiler argument file:
- `ReactCodegen` and `ReactAppDependencyProvider`, Release: `-Os`, then `-O2`. The last flag wins.
- The app target and `ReactAppHeaders`, Release: `-Os` only.
- `ReactCodegen` and `ReactAppDependencyProvider`, Debug: `-O0` only.
**Not tested:**
- `pod install` in prebuilt mode. No prebuilt frameworks exist for the development version, so it fell back to source. In prebuilt mode the React pods contain only binaries, so the setting has no effect there.
- Layout speed on a physical iPhone, and in RNTester.
- The Ruby CocoaPods unit tests, which are not run in CI.
Reviewed By: andrewdacenko
Differential Revision: D126452701
Pulled By: cipolleschi
fbshipit-source-id: d1b4296200a37300f0d6134cc8f922b4c3754c61
(cherry picked from commit 60c817e)
Conflict resolution for 0.88-stable: added only the -O2 flags, without the main-only Yoga excludedPaths and codegen RN_ALLOW_FRAMEWORKS define.
(cherry picked from commit f407034)
tvinhas
pushed a commit
to tvinhas/react-native-macos
that referenced
this pull request
Oct 28, 2026
Summary:
iOS Release builds compile React Native's C, C++ and Objective-C code with `-Os` (optimize for size). React Native never sets `GCC_OPTIMIZATION_LEVEL`, so Xcode's default applies. Android compiles the same code with `-O2` (optimize for speed) in Release. Yoga even sets `-O2` explicitly ("prioritize speed", `ReactCommon/yoga/cmake/project-defaults.cmake`).
This PR sets `GCC_OPTIMIZATION_LEVEL = 2` for Release builds of React Native's own code on iOS, to match Android. Android also builds Yoga and glog at `-O2` in Debug builds, so this PR does the same on iOS. All other code keeps `-O0` in Debug. Hermes keeps its size-optimized `MinSizeRel` build, as on Android. Swift code is not affected.
In a layout benchmark on Apple Silicon, a Yoga layout takes 21–54% less time at `-O2` than at `-Os` in Release. In Debug it takes about 94% less time at `-O2` than at `-O0` (16–19 times faster). The stripped React binary grows by 6.2%.
After this PR, every library that both platforms compile uses the same optimization level: React Native core, Yoga, folly, glog, fmt, boost and double-conversion. Two differences stay out of scope: Android also uses link-time optimization, and Android turns off Yoga's assertions in Debug builds.
I found no commit that chose `-Os` for iOS on purpose. The earlier optimization decisions were all for Android: `c04ab921924` (2016, "Use -O3 to compile CSSLayout in open source … This is how we compile internally") and react#37371 (2023, Yoga's reference CMake build at `-O2`).
The question came up in the community. A user profiling Bluesky found Yoga about 20% faster at `-O2` and layout 2–5% faster in the full app: bluesky-social/social-app#12061. Their app-level workaround is expo/expo#51770. An app-level setting cannot reach the prebuilt React Native frameworks, so the fix belongs here.
### What changes
| Build path | Change |
|---|---|
| Prebuilt `React.xcframework` (`packages/react-native/scripts/ios-prebuild/build.js`) | Adds `GCC_OPTIMIZATION_LEVEL=2` to the `xcodebuild` command for the Release flavor only. |
| Prebuilt `ReactNativeDependencies.xcframework` (`scripts/releases/ios-prebuild/build.js`) | Same, for folly, glog, fmt, boost, double-conversion, fast_float and SocketRocket. |
| CI cache (`.github/workflows/prebuild-ios-dependencies.yml`) | The slice and xcframework cache keys now include `scripts/releases/ios-prebuild/build.js`. Without this, CI reuses the cached `-Os` build. |
| Yoga in `React.xcframework` (`packages/react-native/Package.swift`) | The `yoga` target gets `-O2` in every configuration, as on Android (`ReactCommon/yoga/CMakeLists.txt`). |
| glog in `ReactNativeDependencies.xcframework` (`scripts/releases/ios-prebuild/configuration.js`) | glog gets `-O2` in every configuration, as on Android (`ReactAndroid/src/main/jni/third-party/glog/CMakeLists.txt`). |
| CocoaPods builds from source (`react_native_post_install`) | New step `set_optimization_level_for_react_native_pods`. It sets level 2 on Release-type configurations of React Native pods, and on every configuration of `Yoga` and `glog`. |
| SwiftPM codegen package (`packages/react-native/scripts/codegen/templates/Package.swift.spm-template`) | `ReactCodegen` and `ReactAppDependencyProvider` get `-O2` in Release through `unsafeFlags`, as the CocoaPods generated pods do. SwiftPM allows `unsafeFlags` here because the app always uses this package from a local path. |
The command-line setting applies to every configuration, so the prebuild scripts add it only when they build Release. The per-target `-O2` for Yoga and glog comes after the configuration's `-O0` on the compiler command line, so it wins in Debug.
In CocoaPods, a React Native pod is one of these:
- a local pod inside the `react-native` package
- one of React Native's third-party dependency pods (`RCT-Folly`, `glog`, `boost`, `DoubleConversion`, `fmt`, `fast_float`, `SocketRocket`)
- one of the generated pods `ReactCodegen` and `ReactAppDependencyProvider`
Community library pods, the app's own pods and `hermes-engine` do not change. Release-type configurations are found by `config.type == :release`, as `add_ndebug_flag_to_pods_in_release` already does, so custom configurations such as "Staging" are included.
An app that wants the old behaviour can set `GCC_OPTIMIZATION_LEVEL` back to `s` in its own `post_install` after `react_native_post_install`.
### Benchmark: layout speed
Yoga built twice from this branch with Apple clang 21.0.0 (`-std=c++20 -DNDEBUG -arch arm64`); only the optimization level differs. The benchmark harness is the same object file in both builds. Every iteration forces a full relayout (root width alternates 390/391). There were 10 runs of each build, alternating. Both builds produce identical layout results.
Machine: Apple M4 Max, macOS 27.0.1. This is a Mac, not an iPhone, and the trees are synthetic.
| Scenario | `-Os` (µs per layout, median) | `-O2` (µs per layout, median) | Time reduction |
|---|---|---|---|
| Feed: 500 rows, each with an avatar, 3 measured text nodes and 4 buttons | 2,879 | 2,261 | 21% |
| Deep: 15 levels, mixed row/column, padding, margin, percent widths | 1,098 | 805 | 27% |
| Grid: 1,000 wrapped items with `aspectRatio` | 1,018 | 472 | 54% |
"Time reduction" = 1 − median(`-O2`) / median(`-Os`). The spread between the fastest and slowest of the 10 runs was 3–11%.
Debug builds, same harness, built without `NDEBUG` so Yoga's assertions stay on, as in an iOS Debug build. Median of 3 alternating runs:
| Scenario | `-O0` (µs per layout) | `-O2` (µs per layout) | Time reduction |
|---|---|---|---|
| Feed | 36,350 | 2,310 | 94% (15.7× faster) |
| Deep | 15,184 | 816 | 95% (18.6× faster) |
| Grid | 8,190 | 486 | 94% (16.9× faster) |
### Benchmark: binary size
Release, `ios-arm64` device slice. Sizes are uncompressed; App Store download size will differ.
| Binary | `-Os` (bytes) | `-O2` (bytes) | Difference |
|---|---|---|---|
| `React.framework/React`, stripped (`strip -x`) | 6,154,912 | 6,537,576 | +382,664 (+6.2%) |
| `React.framework/React`, `__TEXT` segment | 4,931,584 | 5,324,800 | +393,216 (+8.0%) |
| `ReactNativeDependencies.framework`, stripped | 778,240 | 873,296 | +95,056 (+12.2%) |
| Yoga only, `__text` section | 77,140 | 112,188 | +35,048 (+45%) |
The unstripped React binary gets *smaller* (10,785,672 → 10,335,408 bytes). More inlining at `-O2` leaves about 4,950 fewer symbols, so the link-edit data shrinks more than the code grows. The stripped size is the relevant number.
If only Yoga were moved to `-O2`, the stripped React binary would grow by 32,776 bytes (+0.53%).
## Changelog:
[IOS] [CHANGED] - Compile React Native's C/C++/Objective-C code with `-O2` instead of `-Os` in Release, and Yoga and glog with `-O2` in all builds, to match Android
Pull Request resolved: react#59226
Test Plan:
**Unit tests.** New Jest tests check that the Release command has `GCC_OPTIMIZATION_LEVEL=2`, the Debug command does not, and the rest of each command is unchanged. They failed before the change and pass after it.
```
yarn jest packages/react-native/scripts/ios-prebuild/__tests__/build-test.js scripts/releases/ios-prebuild/__tests__/build-test.js
Tests: 8 passed, 8 total
```
A new Jest test checks that the SwiftPM codegen template gives `ReactCodegen` and `ReactAppDependencyProvider` the Release-only `-O2` flag in both `cSettings` and `cxxSettings`, and that `ReactAppHeaders` does not get it. It failed before the change and passes after it.
```
yarn jest packages/react-native/scripts/spm/__tests__/codegen-template-optimization-test.js
Tests: 5 passed, 5 total
```
All tests in `packages/react-native/scripts/ios-prebuild` and `scripts/releases`: 24 suites, 196 tests passed. All tests in `packages/react-native/scripts/spm`: 23 suites, 1060 tests passed. ESLint, Prettier and Flow report no errors on the changed files.
**Prebuilt frameworks.** I built `React.xcframework` (`node scripts/ios-prebuild --setup --build --compose --platforms ios --flavor <Debug|Release>`) and `ReactNativeDependencies.xcframework` (`node scripts/releases/prepare-ios-prebuilds.js --setup --build --compose --platforms ios --configuration <Debug|Release>`), then read the last `-O` flag in each target's compiler argument file:
| Build | React Native code | Yoga | glog | Other dependencies |
|---|---|---|---|---|
| Release, before this PR | `-Os` | `-Os` | `-Os` | `-Os` |
| Release, with this PR | `-O2` | `-O2` | `-O2` | `-O2` |
| Debug, with this PR | `-O0` | `-O2` | `-O2` | `-O0` |
**CocoaPods.** `RCT_USE_PREBUILT_RNCORE=0 RCT_USE_RN_DEP=0 bundle exec pod install` in `packages/rn-tester`, then read `Pods.xcodeproj`:
- `Yoga` and `glog`: `GCC_OPTIMIZATION_LEVEL = 2` in Debug and Release.
- Other React Native targets (`React-*`, `RCT-Folly`, `ReactCodegen`, `SocketRocket`, …): 2 in Release, unset in Debug.
- RNTester's own pods (`ScreenshotManager`, `MyNativeView`, `NativeCxxModuleExample`, …) and the app targets: unset in both configurations.
**SwiftPM.** In `private/helloworld`: `node ../../packages/react-native/cli.js spm add --deintegrate --version nightly`, then `xcodebuild -configuration Release -sdk iphonesimulator build`. The build succeeded, and Xcode accepted `unsafeFlags` in the generated package. The `-O` flags in each target's compiler argument file:
- `ReactCodegen` and `ReactAppDependencyProvider`, Release: `-Os`, then `-O2`. The last flag wins.
- The app target and `ReactAppHeaders`, Release: `-Os` only.
- `ReactCodegen` and `ReactAppDependencyProvider`, Debug: `-O0` only.
**Not tested:**
- `pod install` in prebuilt mode. No prebuilt frameworks exist for the development version, so it fell back to source. In prebuilt mode the React pods contain only binaries, so the setting has no effect there.
- Layout speed on a physical iPhone, and in RNTester.
- The Ruby CocoaPods unit tests, which are not run in CI.
Reviewed By: andrewdacenko
Differential Revision: D126452701
Pulled By: cipolleschi
fbshipit-source-id: d1b4296200a37300f0d6134cc8f922b4c3754c61
(cherry picked from commit 60c817e)
Conflict resolution for 0.88-stable: added only the -O2 flags, without the main-only Yoga excludedPaths and codegen RN_ALLOW_FRAMEWORKS define.
(cherry picked from commit f407034)
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
iOS Release builds compile React Native's C, C++ and Objective-C code with
-Os(optimize for size). React Native never setsGCC_OPTIMIZATION_LEVEL, so Xcode's default applies. Android compiles the same code with-O2(optimize for speed) in Release. Yoga even sets-O2explicitly ("prioritize speed",ReactCommon/yoga/cmake/project-defaults.cmake).This PR sets
GCC_OPTIMIZATION_LEVEL = 2for Release builds of React Native's own code on iOS, to match Android. Android also builds Yoga and glog at-O2in Debug builds, so this PR does the same on iOS. All other code keeps-O0in Debug. Hermes keeps its size-optimizedMinSizeRelbuild, as on Android. Swift code is not affected.In a layout benchmark on Apple Silicon, a Yoga layout takes 21–54% less time at
-O2than at-Osin Release. In Debug it takes about 94% less time at-O2than at-O0(16–19 times faster). The stripped React binary grows by 6.2%.After this PR, every library that both platforms compile uses the same optimization level: React Native core, Yoga, folly, glog, fmt, boost and double-conversion. Two differences stay out of scope: Android also uses link-time optimization, and Android turns off Yoga's assertions in Debug builds.
I found no commit that chose
-Osfor iOS on purpose. The earlier optimization decisions were all for Android:c04ab921924(2016, "Use -O3 to compile CSSLayout in open source … This is how we compile internally") and #37371 (2023, Yoga's reference CMake build at-O2).The question came up in the community. A user profiling Bluesky found Yoga about 20% faster at
-O2and layout 2–5% faster in the full app: bluesky-social/social-app#12061. Their app-level workaround is expo/expo#51770. An app-level setting cannot reach the prebuilt React Native frameworks, so the fix belongs here.What changes
React.xcframework(packages/react-native/scripts/ios-prebuild/build.js)GCC_OPTIMIZATION_LEVEL=2to thexcodebuildcommand for the Release flavor only.ReactNativeDependencies.xcframework(scripts/releases/ios-prebuild/build.js).github/workflows/prebuild-ios-dependencies.yml)scripts/releases/ios-prebuild/build.js. Without this, CI reuses the cached-Osbuild.React.xcframework(packages/react-native/Package.swift)yogatarget gets-O2in every configuration, as on Android (ReactCommon/yoga/CMakeLists.txt).ReactNativeDependencies.xcframework(scripts/releases/ios-prebuild/configuration.js)-O2in every configuration, as on Android (ReactAndroid/src/main/jni/third-party/glog/CMakeLists.txt).react_native_post_install)set_optimization_level_for_react_native_pods. It sets level 2 on Release-type configurations of React Native pods, and on every configuration ofYogaandglog.packages/react-native/scripts/codegen/templates/Package.swift.spm-template)ReactCodegenandReactAppDependencyProviderget-O2in Release throughunsafeFlags, as the CocoaPods generated pods do. SwiftPM allowsunsafeFlagshere because the app always uses this package from a local path.The command-line setting applies to every configuration, so the prebuild scripts add it only when they build Release. The per-target
-O2for Yoga and glog comes after the configuration's-O0on the compiler command line, so it wins in Debug.In CocoaPods, a React Native pod is one of these:
react-nativepackageRCT-Folly,glog,boost,DoubleConversion,fmt,fast_float,SocketRocket)ReactCodegenandReactAppDependencyProviderCommunity library pods, the app's own pods and
hermes-enginedo not change. Release-type configurations are found byconfig.type == :release, asadd_ndebug_flag_to_pods_in_releasealready does, so custom configurations such as "Staging" are included.An app that wants the old behaviour can set
GCC_OPTIMIZATION_LEVELback tosin its ownpost_installafterreact_native_post_install.Benchmark: layout speed
Yoga built twice from this branch with Apple clang 21.0.0 (
-std=c++20 -DNDEBUG -arch arm64); only the optimization level differs. The benchmark harness is the same object file in both builds. Every iteration forces a full relayout (root width alternates 390/391). There were 10 runs of each build, alternating. Both builds produce identical layout results.Machine: Apple M4 Max, macOS 27.0.1. This is a Mac, not an iPhone, and the trees are synthetic.
-Os(µs per layout, median)-O2(µs per layout, median)aspectRatio"Time reduction" = 1 − median(
-O2) / median(-Os). The spread between the fastest and slowest of the 10 runs was 3–11%.Debug builds, same harness, built without
NDEBUGso Yoga's assertions stay on, as in an iOS Debug build. Median of 3 alternating runs:-O0(µs per layout)-O2(µs per layout)Benchmark: binary size
Release,
ios-arm64device slice. Sizes are uncompressed; App Store download size will differ.-Os(bytes)-O2(bytes)React.framework/React, stripped (strip -x)React.framework/React,__TEXTsegmentReactNativeDependencies.framework, stripped__textsectionThe unstripped React binary gets smaller (10,785,672 → 10,335,408 bytes). More inlining at
-O2leaves about 4,950 fewer symbols, so the link-edit data shrinks more than the code grows. The stripped size is the relevant number.If only Yoga were moved to
-O2, the stripped React binary would grow by 32,776 bytes (+0.53%).Changelog:
[IOS] [CHANGED] - Compile React Native's C/C++/Objective-C code with
-O2instead of-Osin Release, and Yoga and glog with-O2in all builds, to match AndroidTest Plan:
Unit tests. New Jest tests check that the Release command has
GCC_OPTIMIZATION_LEVEL=2, the Debug command does not, and the rest of each command is unchanged. They failed before the change and pass after it.A new Jest test checks that the SwiftPM codegen template gives
ReactCodegenandReactAppDependencyProviderthe Release-only-O2flag in bothcSettingsandcxxSettings, and thatReactAppHeadersdoes not get it. It failed before the change and passes after it.All tests in
packages/react-native/scripts/ios-prebuildandscripts/releases: 24 suites, 196 tests passed. All tests inpackages/react-native/scripts/spm: 23 suites, 1060 tests passed. ESLint, Prettier and Flow report no errors on the changed files.Prebuilt frameworks. I built
React.xcframework(node scripts/ios-prebuild --setup --build --compose --platforms ios --flavor <Debug|Release>) andReactNativeDependencies.xcframework(node scripts/releases/prepare-ios-prebuilds.js --setup --build --compose --platforms ios --configuration <Debug|Release>), then read the last-Oflag in each target's compiler argument file:-Os-Os-Os-Os-O2-O2-O2-O2-O0-O2-O2-O0CocoaPods.
RCT_USE_PREBUILT_RNCORE=0 RCT_USE_RN_DEP=0 bundle exec pod installinpackages/rn-tester, then readPods.xcodeproj:Yogaandglog:GCC_OPTIMIZATION_LEVEL = 2in Debug and Release.React-*,RCT-Folly,ReactCodegen,SocketRocket, …): 2 in Release, unset in Debug.ScreenshotManager,MyNativeView,NativeCxxModuleExample, …) and the app targets: unset in both configurations.SwiftPM. In
private/helloworld:node ../../packages/react-native/cli.js spm add --deintegrate --version nightly, thenxcodebuild -configuration Release -sdk iphonesimulator build. The build succeeded, and Xcode acceptedunsafeFlagsin the generated package. The-Oflags in each target's compiler argument file:ReactCodegenandReactAppDependencyProvider, Release:-Os, then-O2. The last flag wins.ReactAppHeaders, Release:-Osonly.ReactCodegenandReactAppDependencyProvider, Debug:-O0only.Not tested:
pod installin prebuilt mode. No prebuilt frameworks exist for the development version, so it fell back to source. In prebuilt mode the React pods contain only binaries, so the setting has no effect there.