Skip to content

Compile React Native C/C++ at -O2 on iOS to match Android - #59226

Open
chrfalch wants to merge 2 commits into
mainfrom
chrfalch/ios-cxx-optimization-level
Open

chrfalch wants to merge 2 commits into
mainfrom
chrfalch/ios-cxx-optimization-level

Conversation

@chrfalch

@chrfalch chrfalch commented Oct 20, 2026 •

Copy link
Copy Markdown
Collaborator

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

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.

@meta-cla meta-cla Bot added the CLA Signed This label is managed by the Facebook bot. Authors need to sign the CLA before a PR can be reviewed. label Oct 20, 2026
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
chrfalch force-pushed the chrfalch/ios-cxx-optimization-level branch from 7812049 to ed6c266 Compare October 20, 2026 13:49
@tomekzaw

Copy link
Copy Markdown
Contributor

Amazing work! Congrats! 👏

@cipolleschi cipolleschi left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for working on this.
I left a comment for Cocoapods.

Comment thread packages/react-native/scripts/cocoapods/utils.rb
@chrfalch
chrfalch marked this pull request as ready for review October 20, 2026 15:40
@facebook-github-tools facebook-github-tools Bot added the Shared with Meta Applied via automation to indicate that an Issue or Pull Request has been shared with the team. label Oct 20, 2026
@meta-codesync

meta-codesync Bot commented Oct 20, 2026

Copy link
Copy Markdown

@cipolleschi has imported this pull request. If you are a Meta employee, you can view this in D126452701.

This branch has not been deployed

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

Labels

CLA Signed This label is managed by the Facebook bot. Authors need to sign the CLA before a PR can be reviewed. p: Expo Partner: Expo Partner Shared with Meta Applied via automation to indicate that an Issue or Pull Request has been shared with the team.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants