Context
React Native launched in 2015 with WebKit’s JavaScriptCore as the JS runtime. JavaScriptCore is shipped as part of iOS, and since it is open source, React Native uses a custom fork of JSC in React Native Android apps.
Hermes is a JavaScript engine that is optimized for React Native (https://www.internalfb.com/intern/wiki/Hermes/FAQ/). It has been powering Fb4a and Wilde since 2018, and was announced publicly in 2019 with opt-in support for React Native apps on Android. In 2021, opt-in support for iOS was announced, but JavaScriptCore remained as the default runtime for all React Native apps in open source.
Over the past year, the React Native and Hermes teams have been working together towards making Hermes the default engine. This past summer we announced the upcoming release of React Native 0.70 with Hermes as the default engine for React Native. Now that RN 0.70 has been released, and with additional improvements shipping soon with RN 0.71, our Hermes / React Native integration projects will be wrapping up. I figured this is a good time to write up the projects that we worked on to get to this point.
What we did in 2022
In the React Technologies Org, we plan around two-month long milestones. Early in 2022, we created a plan that would get us to Hermes as the default in three milestones. Then, once Hermes became the default, we created a plan to further integrate Hermes into the iOS build process for the next three milestones. Around the same time, the React Technologies Org shifted to a strategic pillar approach, with these projects falling under the Industry Alignment strategic pillar in the React Technologies Org.
The goal of the Industry Alignment pillar is to ensure that the products of React Technologies can be more easily adopted and extended by our open source ecosystem, by aligning our engineering practices with industry standards.
H1 2022: Integrating Hermes into the React Native Release Process
Under the new architecture, React Native uses JSI as a lightweight API layer to communicate with the JS runtime. If Hermes is built with a different version of JSI than React Native, this can lead to bugs or crashes.
A copy of JSI is automatically synced from xplat/jsi
to xplat/hermes/jsi
and xplat/js/react-native-github/ReactCommon/jsi
. When we build a React Native app with Hermes in the fbsource
monorepo, we can be certain that JSI is in sync across Hermes and React Native. Things are different in open source, however, as Hermes is hosted on a separate git repository from React Native, and each project has its own release process.
When opt-in support for Hermes was added to React Native, the version of Hermes that was used by React Native was managed like any other third-party dependency from npm or CocoaPods. Before creating a new React Native release, we would need to release a new version of Hermes, and bump the Hermes dependency in React Native. This required close coordination in order to minimize the delta in JSI between Hermes and React Native.
If we were going to make Hermes the default engine, we needed to make sure there was no room for introducing disparities in JSI between Hermes and React Native. We would need to integrate Hermes into the React Native release process.
Milestone 1 – Goal: Prototype build from source on Android and iOS
The project plan was formulated as part of the first milestone. React Native would no longer consume Hermes as a third-party dependency. React Native would stop relying on the Hermes release process, and instead we would build Hermes from source as part of creating a React Native release. This would allow us to build Hermes with the same version of JSI that is synced to the React Native open source repository.
We developed the initial implementations for building an arbitrary copy of Hermes from source with Gradle on Android and with CocoaPods on iOS. With the prototype working, we declared the Milestone 1 completed and set out to finish the integration in Milestone 2.
Milestone 2 – Goal: Hermes is built from source, on either Android or iOS, in an open source React Native release
With the prototypes ready, we started integrating Hermes into the React Native release process. First off, when React Native itself is built, we download the source code for Hermes from its open source git repository, and then we build Hermes from source but using the same copy of JSI from the React Native open source git repository instead of the copy of JSI from the Hermes repository.
Then, before creating a React Native release, we create a git tag on the Hermes repository, and store a reference to this tag in the React Native release itself. With this, we can make sure that we pin the version of Hermes that is used for each React Native release.
This process was integrated into React Native’s Circle CI. Now, for every commit that syncs to React Native in open source, we are able to test that Hermes can be built, and that all of the open source tests are green when React Native uses that specific version of Hermes. This allows us to catch any regressions right away, when before these may have remained undetected until the Hermes dependency was bumped prior to creating a React Native release.
With support for building Hermes from source now available on both Android and iOS, we considered Milestone 2 to be completed successfully. These changes eventually made it into an open source release when React Native 0.69 was released in June, marking the first version of React Native to ship with "Bundled Hermes" on both Android and iOS.
Milestone 3 – Goal: Hermes is publicly announced as the default engine on either Android or iOS
One of the blockers towards making Hermes the default on iOS was lack of support for the Internationalization APIs (Intl
). Most of the missing Intl
functions were added to iOS by the end of this milestone.
Another blocker to making Hermes the default was extremely long build times on iOS because we needed to build it four times: once for each of the supported Apple platforms (iOS device, iOS Simulator, macOS, and MacCatalyst). Before, all four would be built on Circle CI as part of the Hermes release process, and these prebuilt artifacts would be downloaded by CocoaPods from the Hermes release on GitHub. So to resolve this issue, we now also generate prebuilt artifacts on React Native’s Circle CI, which are uploaded as part of the GitHub Release.
With this, people using Hermes on a published React Native release would not need to re-build it from source. We made the switch to make Hermes the default engine on both Android and iOS; the next public release of React Native, 0.70, would ship with Hermes as the default. Although the public announcement happened a week after Milestone 3 ended, we still considered this a success as the changes on the React Native side were ready on time and the timing of the announcement was partly determined by our Comms review process with Apple.
H2 2022: Hermes iOS Build Integration
With Hermes now the default for all React Native apps, we set out to determine the remaining work that needed to be done to provide a great experience when using Hermes on iOS. We landed on these issues:
- When Hermes is enabled, source maps are not output, which leads to increased bundle sizes.
- Integrating Hermes into the Xcode build pipeline would allow us to build Hermes for the targeted platform, mitigating the long build times on iOS when prebuilts are not available.
- The Hermes debugger is always included, which lead to increased build app sizes.
- The prebuilt Hermes artifacts do not contain debug symbols for iOS, which prevents symbolication of Hermes stack traces.
- Although we now use the same source code when JSI is built by Hermes and by React Native, we still had two separate JSI static libraries, causing a violation of the C++ One Definition Rule.
Milestone 4 Goal: Output source maps
Fixing the source maps was allotted to Milestone 4. Source maps are now output when Hermes is built, so this short Milestone was considered a success.
Milestone 5 Removing the Hermes debugger, integrating with Xcode, solving ODR, and testing on Sandcastle
Most of the rest of the remaining work was allotted to Milestone 5, with support for symbolication of stack traces allotted to Milestone 6.
Building Hermes is now done as part of a Xcode build phase. This allows us to observe the target build parameters. Instead of pre-building Hermes for all supported platforms, we now only build Hermes for the current target, leading to massive savings in total build time when prebuilt artifacts are not available. The only artifact that is still built during pod install
is hermesc
, since it needs to be built for macOS regardless of which platform the React Native app will target.
The ODR violation with JSI was resolved by splitting JavaScriptCore out of React-jsi
and into its own React-jsc
Pod, then updating the CocoaPods configuration for React-jsi
so that React Native does not build its own copy of JSI when Hermes is enabled.
To avoid shipping the Hermes debugger in release builds, we created separate Hermes prebuilts for debug and for release. Now, only the debug Hermes builds ship with the Hermes debugger.
Finally, we added Hermes variants to our open source tests on Sandcastle to catch regressions at diff and land time – not just when these get synced to open source.
All of this work was completed successfully in Milestone 5.
Milestone 6 Debug Symbols
Although we are only one week into Milestone 6, we have already completed the remaining work. Symbolication of Hermes stack traces on iOS is now possible, with debug symbols now forming part of the release artifacts for a React Native release. The dSYMs are provided in a separate tarball, and we will document the process for getting these in the React Native docs.
We expect these changes to be cherry-picked into the ongoing 0.71 release candidate.
Summary
In 2022, we finally transitioned Hermes from being opt-in, to now becoming the default engine for all React Native apps.
Great #thanks to Nicola, Dmitry, Riccardo, Neil, John, Michael, and everyone on the Hermes team who helped us get to this point, and thanks to Kevin, Aleksandar, Seth, and Yuzhi for supporting us.
Future Potential Projects
Now that Hermes is the default engine, the "Integrating Hermes into the React Native Release Process" project is considered completed. As we work on releasing the last set of Hermes iOS changes in React Native 0.71, we will be wrapping up the "Hermes iOS Build Integration" project for 2022. Here are some additional projects we may want to consider as we start planning for 2023.
Re-thinking hermes-engine
Pod
Our configuration for the Hermes Pod has grown quite complex, with different code paths depending on whether we are using prebuilts or building Hermes from source. We may want to consider splitting the Pod into "Hermes with Prebuilts" and "Hermes Build from Source", and let our CocoaPods scripts configure the project with the correct Pod dependency.
Shipping Prebuilts with Debug Symbols
The debug symbols for iOS are stripped from the Hermes prebuilts, and they are distributed as a separate release artifact. This saves people from downloading the debug symbols until they need them, but still leaves us with a manual process for downloading them when the need comes.
If these debug symbols are not stripped from the Hermes prebuilts, they should get automatically picked up by CocoaPods and configured as part of the Xcode workspace. We could have a separate "Hermes with Debug Symbols" pod that makes use of a Hermes prebuilts tarball were the dSYMs are not stripped out.
Removing JSI when building Hermes for React Native
Since Hermes builds its own JSI library, the fix for the JSI ODR violation required making the React-jsi
Pod a no-op when Hermes is enabled. While this gets the job done, it is not the cleanest implementation. Ideally, React-jsi
would always provide JSI, and Hermes would not be built with JSI as part of the hermes-engine
Pod. To do this, we’d need to either create a new Hermes build target that does not link Hermes with JSI, or we’d need to finalize support for use_frameworks!
on iOS so that we can build Hermes with JSI as a shared library, with React-jsi
‘s own JSI dylib getting linked at runtime.
Finalizing Intl
support
Hermes Intl
support on iOS is not yet complete, as formatToParts
is still missing.
Hardening the React Native release process
While this falls under the larger Industry Alignment pillar, now that Hermes is integrated into the React Native release process, I figure it is worth covering here. We typically run into unexpected issues while creating a new React Native release. This may be due to manual steps in the process, or configuration changes that are specific to a release which are not tested as part of the general suite of open source tests.
We have started working on creating a React Native release as a "dry run" for every commit, but there’s still a lot of work that can be done. For example, when creating a React Native release, we first build Hermes from source, then we bump the React Native version number. This resulted in a bug while creating the 0.71 release due to the Hermes prebuilts artifacts having a different version number than the React Native version being released. This was not caught earlier by our "dry run" because upload artifacts is intentionally skipped.