React Native Info

Home Awsomeness Contact

Using React Native in Games (In-Game Ads) - Performance Optimization

Translated from:

In this context, “cross app” refers to the ability to share code and software modules across different executables on different platforms. It’s an advanced version of cross-platform development.

** Author: Bytedance Front End Platform Team - 熊文源**

The React Native based cross-app architecture enhances the delivery speed. However, problems arise in different game runtime environments when it scales. This is expected due to the complexity involved in the use case of RN in games. The key challenges are immersive experience, bootstrap latency, memory footprint, and rendering speed. These are known issues to React Native, hence we started optimization case-by-case to address them.

Bootstrap Latency

We test with a large volume of data in preparing for the actual optimization work. The React Native performance is good in ordinary apps. However, in games where low memory and highly saturated CPU are the norms, the issue of bootstrap latency becomes very notable. Let’s understand first the steps that are time consuming.

The bootstrap of React Native enlists 1) load the Core Bridge which encapsulates runtime, UI, and API components; 2) execute the domain logic in JavaScript and render the React components; 3) render the native UI elements in accordance with the components. Preloading of Core Bridge is a common tackle to improve bootstrap speed.

  1. Full bundle preload (bridge only): We generate and cache ReactInstanceManager with the preloaded bundle. We load the instance directly from memory and bind it with rootview when the respective page is invoked. This optimisation enhances the bootstrap speed by 30% - 50%. In games, we achieve <1s bootstrap in physical device, and <2s in emulators. Nonetheless, this particular method of “time–memory trade-off” is not scalable because space complexity is O(n) where n is the number of bundles.
  2. Common bundle preload (bridge only): To address the above limitation we adopt bundle split so common bundle can be preloaded separately. The research shows that the React Native bundle can be splitted into common and business bundle. Common bundle contains stock components and libraries, and business bundle contains the business components and logic. We split the complete package into common (x1) + business (xN) by altering the packaging mechanism. We preload common package and cache the ReactInstanceManager, and only load business package on demanded. It gives a 15% - 20% enhancement (react-native-multibundler).
  3. Rootview preload: JavaScript also takes some time bootstrap. We take one step further and reload rootview (with common package). Presumably this approach consumes more memory than preloading of just bridge and it achieves almost 0 TTI.

All of the aforementioned approaches utilize memory to enhance latency, and they can all be feature toggled from the server side. Additionally, it is worth noting the following heuristic related to bootstrap time:

  1. Lazy module, ~5% improvements by altering the stock Native Modules to lazy module
  2. On-demand require: Utilize lazy require for non-UI business logic to optimize loading.
  3. Package tailoring: Exclude unused React modules, APIs, and components from the domain logic to minimise the package (the smaller, the faster).
  4. Optimize package splitting strategy: After full minimization, consider further splitting the package by page to further enhance bootstrap time.
  5. Prefetch network requests that are on the critical path of first screen TTI, nd cache the response.
  6. Prefetch images and cache the binary in image cache such as Fresco.

In addition to the aforementioned approaches, replacing JSC with Hermes can yield significant improvements in bootstrap performance. Detailed discussions on Hermes will be provided in upcoming posts.

Memory Optimisation

The above heuristics that target bootstrap, are common to industry. The complex runtime of games has strict demands on memory consumption. This is because games generally consume more memory than ordinary apps.

  1. Package splitting: Besides bootstrap gains, package splitting also optimizes memory usage by loading packages into memory only when they are requested.
  2. Fonts: Since fonts cannot be shared between the game and native environments, utilizing fonts in React Native pages can result in increased memory consumption. To mitigate this, we tailor the font files and only load the necessary characters. Additionally, we provide support for dynamically loading fonts from the server side.
  3. Graphic: Graphics are one of the leading factors contributing to high memory consumption. Additionally, graphic normally enlists cache that persists the memory consumption even when not displayed in the view port. We provide support of webp, gif, as well as employ lossy compression. Simultaneously, we support backend-driven media queries. We also provide API for proactive cache cleaning.

Rendering Speed

Rendering performance plays a vital role in games. Due to the constrained runtime of games, characterized by high CPU load, React Native pages tend to be less performant in comparison to native-powered pages with the same level of layout complexity. To address this, following we list the critical points for React Native rendering optimisation that ensures support for consistent 60 FPS performance in the majority of cases.

  1. In React Native UI updating in Frame Buffer is triggered for every frame by design. This is not optimal in highly loaded UI thread in games.
  2. Animation and clicking are based on the same mechanism, which increases the latency for each rendering pass.
  3. To tackle the aforementioned challenges, we have implemented a message map system where animations and UI updating events are stored. Here’s how it works. Everytime a) When a frame is rendered, we check for any new messages in the message map; b) If no new messages are found, we can skip scheduling tasks on the UI thread; c) If new messages are present, we utilize the registered callback associated with each message to update the UI accordingly.

Another crucial aspect to consider is hardware acceleration. React Native leverages native UI rendering, which delivers optimal performance when hardware acceleration is enabled. However, in most games, hardware acceleration is disabled due to the utilization of customized rendering modules or engines. Consequently, the FPS (frames per second) and UI responsiveness often fall below the expected quality bar, particularly in emulator (FPS 30). How did we improve the performance in such setting?

  1. Simplifying the UI can greatly improve performance, especially when it comes to background images.
  2. React Native provides the renderToHardwareTextureAndroid option, which utilizes memory for rendering performance. This is a suitable choice for businesses that don’t heavily rely on images and where memory usage is not a critical concern.
  3. For businesses that heavily utilize images, we have designed a module that uses OpenGL rendering method and supports texture formats such as ETC1. This module significantly enhances rendering performance while managing memory consumption. However, please note that it relies on hardware acceleration and is typically used for specific modes or scenarios.