Since we announced Hermes in 2019, it has been increasingly gaining adoption in the community. The team at Expo, who maintain a popular meta-framework for React Native apps, recently announced experimental support for Hermes after being one of the most requested features of Expo. The team at Realm, a popular mobile database, also recently shipped its alpha support for Hermes. In this post, we want to highlight some of the most exciting progress we’ve made over the past two years to push Hermes towards being the best JavaScript engine for React Native. Looking forward, we are confident that with these improvements and more to come, we can make Hermes the default JavaScript engine for React Native across all platforms.
Hermes’s defining feature is how it performs compilation work ahead-of-time, meaning that React Native apps with Hermes enabled ship with precompiled optimized bytecode instead of plain JavaScript source. This drastically reduces the amount of work needed to start up your product for users. Measurements from both Facebook and community apps have suggested that enabling Hermes often cut a product’s TTI (or Time-To-Interactive) metric by nearly half.
That being said, we’ve been working on improving Hermes in many other aspects to make it even better as a JavaScript engine specialized for React Native.
With the upcoming Fabric renderer in the new React Native architecture, it will be possible to synchronously call JavaScript on the UI thread. However, this means if the JavaScript thread takes too long to execute, it can cause noticeable UI frame drops and block user inputs. The concurrent rendering enabled by React Fiber will avoid scheduling long JavaScript tasks by splitting rendering work into chunks. However, there is another common source of latency from the JavaScript thread — when the JavaScript engine has to “stop the world” to perform a garbage collection (GC).
The previous default garbage collector in Hermes, GenGC, was a single-threaded generational garbage collector. The new generations uses a typical semi-space copying strategy, and the old generations uses a mark-compact strategy to make it really good at aggressively returning memory to the operating system. Due to its single-thread, GenGC has the downside of causing long GC pauses. On apps that are as complicated as Facebook for Android, we observed an average pause of 200ms, or 1.4s at p99. We have even seen it be as long as 7 seconds, considering the large and diverse user base of Facebook for Android.
In order to mitigate this, we implemented a brand new mostly concurrent GC named Hades. Hades collects its young generation exactly the same as GenGC, but it manages its old generation with a snapshot-at-the-beginning style mark-sweep collector. which can significantly reduce GC pause time by performing most of its work in a background thread without blocking the engine’s main thread from executing JavaScript code. Our statistics show that Hades only pauses for 48ms at p99.9 on 64-bit devices (34x faster than GenGC!) and around 88ms at p99.9 on 32-bit devices (where it operates as a single-threaded incremental GC). These pause time improvements can come at the cost of overall throughput, due to the need for more expensive write barriers, slower freelist based allocation (as opposed to a bump pointer allocator), and increased heap fragmentation. We think those are the right trade-offs, and we were able to achieve overall lower memory consumption via coalescing and additional memory optimizations that we’ll talk about.
Startup time of applications is critical to the success of many apps, and we are continuously pushing the boundary for React Native. For any new JavaScript feature we implement in Hermes, we carefully monitor their impact on production performance and ensure that they don’t regress metrics. At Facebook, we are currently experimenting with a dedicated Babel transform profile for Hermes in Metro to replace a dozen Babel transforms with Hermes’s native ESNext implementations. We were able to observe 18-25% TTI improvements on many surfaces and overall bytecode size decreases and we expect to see similar results for OSS.
In addition to startup performance, we identified memory footprint as an opportunity for improvement in React Native apps especially for virtual reality. Thanks to the low-level control we have as a JavaScript engine, we were able to deliver rounds of memory optimizations by squeezing bits and bytes out:
One of our key decisions with Hermes was to not implement a just-in-time (JIT) compiler because we believe that for most React Native apps, the additional warm-up costs and extra footprints on binary and memory would not actually be worthwhile. For years, we invested a lot of effort in optimizing interpreter performance and compiler optimizations to make Hermes’s throughput competitive with other engines for React Native workloads. We are continuing to focus on improving throughput by identifying performance bottlenecks from everywhere (interpreter dispatch loop, stack layout, object model, GC, etc.). Expect some more numbers in upcoming releases!
At Facebook, we prefer to colocate projects within a large monorepo. By having the engine (Hermes) and the host (React Native) closely iterating together, we opened a lot of room for vertical integrations. To name a few:
setImmediate
APIs. We are working on making native promises and microtasks from JS engines available via JSI, and introducing queueMicrotask
, a recent addition to the web standard, to the platform, to better support modern asynchronous JavaScript code.Hermes has been really great for us at Facebook. But our work is not done until our community can use Hermes to power experiences throughout the ecosystem, so that everyone leverage all of its features and to embrace its full potential.
Hermes was initially open sourced only for React Native on Android. Since then, we have been thrilled to see our members of the community expanding Hermes support into many other platforms that React Native’s ecosystem has expanded.
Callstack led the effort of bringing Hermes to iOS in React Native 0.64. They wrote a series of articles and hosted a podcast on how they achieved it. According to their benchmarks, Hermes was able to consistently deliver ~40% improvement to startup and ~18% reduced memory on iOS compared to JSC for the Mattermost app, with only 2.4 MiB of app size overhead. I encourage you to see it live with your own eyes.
Microsoft has been bringing Hermes to React Native for Windows and macOS. At Microsoft Build 2020, Microsoft shared that Hermes’s memory impact (working set) is 13% lower than the Chakra engine on React Native for Windows. Recently, on some synthetic benchmarks, they’ve found Hermes 0.8 (shipped with Hades and aforementioned SMI and pointer compression optimization) uses 30%-40% less memory than other engines. Not surprisingly, the desktop Messenger video calling experience built on React Native, is also powered by Hermes.
Last but not least, Hermes has also been powering all virtual reality experiences built with the React family of technologies on Oculus, including Oculus Home.
We acknowledge there are still blockers that prevent parts of the community from adopting Hermes and we are committed to building support for these missing features. Our goal is to be fully featured so that Hermes is the right choice for most React Native apps. Here is how the community has already shaped the Hermes roadmap:
Proxy
and Reflect
were originally excluded from Hermes because Facebook does not use them. We were also concerned that adding Proxy would hurt property lookup performance even when Proxy is not used. But Proxy quickly become the most requested feature of Hermes due to popular libraries such as MobX and Immer. We carefully evaluated and decided to build it just for the community, and we managed to implement it with very low cost. Since this is a feature we don’t use, we relied on our community to prove its stability. We started by testing Proxy behind a flag and created opt-in npm packages for release v0.4 and v0.5, and it’s enabled by default starting from v0.7.Intl
) was the second most requested feature. Intl
is a huge set of APIs and often requires the implementation to include 6MB worth of Unicode CLDR data. This is why polyfills like FormatJS (a.k.a. react-intl
) and JS engines like the international variant build of community JSC are so huge. To avoid substantially increasing the binary size of Hermes, we decided to implement it with another strategy by consuming and mapping the ICU facilities provided by the libraries included in the operating systems, at the cost of some (often minor) variance in behaviors across platforms.Intl
by default, so that’s what we did and it’s available starting from release v0.8.Intl
on iOS. Stayed tuned!Array.prototype.sort
amended in ES2019. This has been fixed and will be available in the next release.Function.prototype.toString
implementation caused performance to drop in libraries doing improper feature detection and blocked users from doing source code injecting. This helped us strengthen our stance that Hermes, whenever possible, should not get in the way of developers and to respect de-facto practices.In summary, our vision is to make Hermes ready to be the default JavaScript engine across all React Native platforms. We’ve already started working towards it, and we want to hear from all of you about this direction.
It’s extremely important for us to prepare the ecosystem for a smooth adoption. We encourage you to try out Hermes, and file issues on our GitHub repository for any feedbacks, questions, feature requests and incompatibilities.
We’d love to thank the Hermes team, the React Native team, and the many contributors from the React Native community for their work to improve Hermes.
I’d also love to personally thank (in alphabetic order) Eli White, Luna Wei, Neil Dhar, Tim Yung, Tzvetan Mikov, and many others for their help during the writing.
Like (0)