Skip to main content

Toward Hermes being the Default

· 12 min read
Xuan Huang

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.

Optimizing for React Native

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.

Building a New Garbage Collector for Fabric

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.

Striking on Performance Pain Points

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:

  1. Previously, all JavaScript values were represented as 64-bit NaN-boxing encoded tagged values to represent floating point doubles and pointers on 64-bit architecture. However, this is wasteful in practice because most numbers are small integers (SMI) and JavaScript heap of client-side applications is not expected to be larger than 4GiB generally. To address this, we introduced a new 32-bit encoding in which SMI and pointers are encoded in 29 bits (because pointers are 8-byte aligned, we can assume the bottom 3 bits are always zero), and the rest of JS numbers are boxed onto the heap. This reduced the JavaScript heap size by ~30%.
  2. Different kinds of JavaScript objects are represented as different kinds of GC-managed cells in the JavaScript heap. By aggressively optimizing the memory layout of headers for those cells, we are able to reduce memory usage by another ~15%.

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!

Pioneering at Vertical Integrations

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:

  • Hermes supports on-device JavaScript debugging with the Chrome debugger by speaking the Chrome DevTools Protocol. It’s better than the legacy “Remote JS Debugging” (which uses an in-app proxy to run JS in desktop Chrome) because it supports debugging synchronous native calls and guaranteed a consistent runtime environment. Together with React DevTools, Metro, Inspector, and so on, Hermes debugger is now part of Flipper to provide a one-stop developer experience.
  • Objects allocated during the initialization path of React Native apps are often long-lived and don’t follow the generational hypothesis leveraged by generational GCs. Therefore, we configured Hermes in React Native to allocate the first 32MiB directly into old generations (known as pre-tenuring) to avoid triggering GC pauses and delaying TTI.
  • The new React Native architecture is heavily based on JSI (or JavaScript Interface), a lightweight, general-purposed API for embedding a JavaScript engine into a C++ program. By having the team maintaining the JS engine also maintains the JSI API implementation, we are confident in providing the best possible integration that is reliable, performant and battle-tested at the Facebook’s scale.
  • Getting JavaScript concurrency primitives (e.g. promises) and platform concurrency primitives (e.g. microtasks) both semantically correct and performant are critical to React concurrent rendering and the future of React Native apps. Historically, promises in React Native were polyfilled using non-standardized 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.

Bringing Along the Whole Community

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.

Expanding to New Platforms

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.

Supporting our Community

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:

Summary

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.

Thanks

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.