Since its first release in 2019, Hermes, a lightweight JavaScript engine, has become increasingly popular in the community, with many frameworks supporting it. The Expo team, the creators of the popular meta-framework for React Native, previously announced experimental support for Hermes. In addition, the team from the popular mobile database Realm recently decided to provide Alpha support for Hermes.
In this article, we want to highlight the exciting progress that has been made over the past two years to make Hermes the best JavaScript engine in React Native. Going forward, we are confident that more improvements will make Hermes the default JavaScript engine for React Native on all platforms.
Optimized for React Native
Function definitions in Hermes are responsible for indicating how to perform compilation ahead of time. In other words, hermetic-enabled React Native applications come with precompiled and optimized bytecodes rather than pure JavaScript source code. This greatly reduces the amount of work required for users to launch the product. Quantitative tests from Facebook and other applications in the community have shown that enabling Hermes typically reduces product TTI (interaction time) metrics by nearly half.
But we’re not stopping there. We’re working on improving Hermes to make it the best JavaScript engine for React Native.
Create a new garbage collector for Fabric
The Fabric renderer featured in the new React Native architecture is one of the most notable features of the Fabric renderer, which calls JavaScript synchronously on the UI thread. However, if the JavaScript thread takes too long to execute, it can cause significant UI frame loss and prevent the user from typing properly.
React Fiber provides a concurrent rendering mechanism that splits the rendering into multiple chunks, thus preventing a single JavaScript task from taking too long. In addition, another common source of latency in JavaScript threads is the garbage collection (GC) mechanism. Because once garbage collection starts, the entire JavaScript engine must drop everything to perform garbage collection.
The original default garbage collector in Hermes, GenGC, is a single-threaded generational garbage collection scheme. A typical half-space copy strategy is used for the new generation, and a Mark-compact strategy is used for the older generation to more proactively return memory to the operating system.
On a complex application like Facebook for Android, we observed an average pause of 200 milliseconds and 1.4 seconds at the 99th percentile. Given Facebook for Android’s large and diverse user base, the most extreme pause times can be as long as seven seconds.
To alleviate this situation, we have built a new, concurrent oriented garbage collection solution, called Hades. Hades also adopts a generational design. The collection method of the new generation is exactly the same as that of GenGC, while the collection method of the old generation is managed by the snapshot-type marker scan collector.
Hades is able to offload most of the workload to background threads, significantly reducing the garbage collection pause time without preventing the main engine thread from continuing to execute JavaScript code. Our statistics show that Hades has a latency of 48 milliseconds at the 99.9th percentile on 64-bit devices (34 times faster than GenGC!). , while the latency at the 99.9th percentile on 32-bit devices is about 88 milliseconds (running as a single-threaded incremental GC).
But by requiring a more resource-expensive write barrier, a slower free list-based allocation mechanism (as opposed to the collision pointer allocator), and more heap fragmentation, Hades is actually trading overall throughput for shorter pause times. We believe this trade-off is consistent with user habits and will achieve a lower overall memory footprint by combining it with other memory optimization mechanisms discussed next.
Improving performance issues
App startup times are critical for many applications, and we want to keep increasing React Native’s performance ceiling. For all JavaScript features implemented in Hermes, we carefully monitor their impact on production performance and make sure they don’t drag down performance metrics.
At Facebook, we are currently experimenting with a dedicated Babel Transform profile for Hermes in Metro, hoping to replace a dozen or so Babel Transforms with native ESNext implementations in Hermes. In this way, we have improved TTI by 18 to 25 percent in direct observation and slimmened the overall bytecode significantly; Hopefully similar results will follow in OSS.
In addition to startup performance, we see memory usage as an important opportunity to improve the React Native application, which is a prerequisite for a good VR experience. Therefore, in the low-level control of JavaScript engine, we use compressed bits and bytes to achieve multiple rounds of memory optimization:
- Previously, all JavaScript values were represented as token values encoded in 64-bit NaN boxes to represent double-precision floating-point values and Pointers on 64-bit schemas. But this approach is a huge waste in practice, because most numbers are actually small integers (SMI), and the JavaScript heap for client applications is usually no larger than 4 GiB. To solve this problem, we introduced a new 32-bit encoding in which SMI is encoded in 29 bits with Pointers (since Pointers are 8-byte aligned, we can assume that the bottom three bits are always zero), and the rest of the JS numbers are boxed onto the heap. This reduces the size of the JavaScript heap by about 30%.
- Different classes of JavaScript objects are represented as different GC snap-ins on the JS heap. By proactively optimizing the memory layout of these cell headers, we were able to further reduce memory footprint by approximately 15%.
We also made a key decision at Hermes to move away from the just-in-time (JIT) compiler. Because we don’t think the extra warm-up costs and extra binaries and even memory footprint are really meaningful for most React Native applications.
Over the years, we have put a lot of effort into interpreter performance and compiler optimizations, and Hermes has gained a throughput advantage over React Native workloads compared to other engines. We will continue to focus on widespread performance bottlenecks (interpreter scheduling loops, stack layouts, object models, garbage collection, and so on) to improve throughput.
Vertically integrated field
At Facebook, we are used to using large Monorepo hosting projects. By closely iterating engine (Hermes) with host (React Native), we now open up a lot of room for vertical integration. Here are a few specific examples:
- Hermes uses the Chrome DevTools protocol to support JavaScript debugging on the device using the Chrome debugger. This approach works better than traditional “remote JS debugging” (running JS on desktop Chrome using an in-app proxy) because it enables debugging to synchronize native calls and ensures a unified runtime environment. Along with React DevTools, Metro, and Inspector, the Hermes debugger is now part of Flipper, providing a one-stop developer experience.
- Objects allocated in the initialization path of React Native applications often exist for a long time and do not conform to the generational assumption proposed by generational GC. Therefore, when we configured Hermes in React Native, we allocated the first 32 MIBs directly to the older years (i.e., pre-tenuring) to avoid triggering GC pauses and delayed TTI.
- The new React Native architecture is based in large part on JSI (JavaScript Interface), a lightweight, general-purpose API for embedding JavaScript engines into C++ applications. By having the same team that maintains the JS engine also maintain the JSI API implementation, we are confident that we can provide the best integration. This integration solution has been proven to be reliable and efficient on a large scale with Facebook.
- Having semantically correct and well-performing JavaScript concurrency primitives (like Promises) and platform concurrency primitives (like MicroTasks) is critical to React concurrent rendering and the future of React Native applications. Historically, promises in React Native used the nonstandardized setImmediate API as putty scripts. We are trying to implement native Promises and Microtasks from the JS engine through JSI and bring queueMicrotask (a new addition to the Web standard) to the platform to better support modern asynchronous JavaScript code.
Community development
Facebook is taking the Hermes project very seriously, but the work won’t really be over until there is a complete ecosystem for Hermes, especially the technical community. Only in this way can everyone make full use of Hermes and realize its potential.
Expand to more new platforms
Hermes was originally open source only for React Native on Android. Since then, we have been pleased to see community members gradually expand support for Hermes and have now extended it to a variety of other platforms covered by the React Native ecosystem.
Callstack first introduced Hermes to iOS in React Native 0.64. They also published a series of feature articles and launched a podcast to show users how they did it. According to the benchmark, Hermes achieved a stable startup performance improvement of about 40% on iOS, a memory footprint reduction of about 18%, and a memory usage of 2.4 MiB during the running of the application compared to JSC for the Mattermost application.
Microsoft continues to introduce Hermes to React Native on Windows and MacOS. At Microsoft’s Build 2020 conference, the software giant revealed that Hermes was able to reduce the memory footprint of React Native for Windows by 13% compared to the original Chakra engine. In some recent comprehensive benchmarks, Microsoft found that Hermes 0.8 (which includes Hades and the previously mentioned SMI and pointer compression optimizations) uses 30% to 40% less memory than other engines. It’s no surprise that the React Native desktop Messenger video calling experience is also significantly improved with Hermes support.
More importantly, Hermes has been supporting a variety of vr experiences built on Oculus using the React family of technologies, including Oculus Home.
Community support
We acknowledge that there are still issues with Hermes that prevent smooth community engagement, and we are working hard to build support for these missing features. Our goal is to get fully functional as soon as possible, making Hermes the best choice for most React Native applications. Here is the Hermes roadmap the community is planning:
-
Proxy and Reflect were initially excluded from Hermes because They are not used by Facebook. We were worried that adding proxies without actually using them would hurt property lookup performance. But with the popularity of libraries such as MobX and Immer, Proxy quickly became the most popular feature in Hermes. After careful evaluation, we decided to provide a dedicated Proxy for the community and managed to do it at very low cost. Since Facebook does not use this feature, it is up to the tech community to prove its stability. We started Proxy testing in the form of marking and creating an opt-in NPM package in versions 0.4 and 0.5, and enabled it by default starting with version 0.7.
-
The ECMAScript internationalization API specification (ECMA-402 or Intl for short) is also at the center of user outcry. Intl represents a large set of apis that typically require 6 MB of Unicode CLDR data to implement. This is why cloying scripts like FormatJS (aka React-Intl) and JS engines like build, the international variant of the community JSC, are so bloated and ungainly. To avoid unnecessary bloat in the size of the Hermes binaries, we decided to directly use and map the ICU facilities provided by the OS built-in libraries, at the expense of introducing some (often minor) differences in certain cross-platform behaviors.
-
Microsoft collaborated on build support for Android. This covers almost everything from ECMA-402 to ES2020, with a volume impact of only 3% (57 K to 62 K per ABI). Our Twitter poll found strong support for Intl by default, so we decided to introduce it starting with version 0.8.
-
Facebook has sponsored Major League Hacking to launch a remote open Source scholarship program. Last year, we launched the Hermes sampling analyzer; This year, our researchers will work with Hermes, React Native, and Callstack members to implement Hermes Intl support on iOS.
-
Thank you for your feedback about the low default heap size cap, which caused unnecessary GC stress and OOM crashes for users unfamiliar with custom Hermes GC configurations. So by default, we increased the upper limit from 512 MiB to 3 GiB, which should be more than enough for most users.
-
There are reports that our special Function. The prototype. The toString implementation leads to library execute incorrect feature detection and lead to performance degradation, and users can’t perform source code injection. In solving the problem, we have strengthened Hermes’ determination to respect the facts and avoid hindering the smooth use of developers.
conclusion
Overall, our vision is to make Hermes the default JavaScript engine on all React Native platforms. We are working in this direction and hope to fully listen to your feedback from different perspectives.
Only by being well prepared can we lay a solid foundation for the wider adoption of Hermes in the ecosystem. Here, we invite you to try Hermes and submit any suggestions, comments, feature requests and incompatibilities you find to our GitHub repo.
Toward Hermes being the Default