Author: Xiong Wenyuan, Byte Game Client team
Client cross-end frameworks have been developed for many years. Recently popular aprons, Flutter and ReactNative are all successful and mature frameworks, which are targeted at different developers and widely used by many large apps. I was fortunate to participate in learning and using these excellent cross-end solutions early. In recent years of development and architecture design, in addition to supporting tens of millions of DAU in the App, ReactNative cross-terminal scheme was gradually applied to the game to improve the efficiency of development and iteration. In this article, we are going to introduce some of our explorations and practices in games in five chapters, which you can learn from:
- Part 1: Background on using ReactNative in games
- How to integrate ReactNative into a game
- ReactNative performance optimization in games
- ReactNative Hermes Engine Introduction
- Chapter 5: Introduction to ReactNative’s new architecture
(This is the third part of a series.)
With the iterative version is perfect, basic has the ability of a large number of online games, with more and more games business, in the different game environment, also met some problems, it also reflects the game scene from the side and the complexity of the architecture, main core problem lies in ReactNative immersive experience, startup performance, memory and rendering performance problems, etc., It seems that these problems are also common problems of ReactNative. In order to solve these problems, we started special optimization.
1. Enable performance optimization
For the problem of startup performance, we also tested a large number of data. ReactNative performs well in pure client App, but this problem is particularly prominent in the case of low memory and excessive CPU usage. To solve these problems, we first need to understand the main time consumption of ReactNative loading. Please refer to the following figure:
Before rendering and displaying the entire page, the ReactNative Core Bridge needs to be loaded and initialized, which mainly contains the ReactNative operating environment, UI and API component functions, etc., before running the JS of the business and executing the render to draw the UI. After completion, React Native can render JS components as Native components. For page loading process is fixed, so we can use the preload in advance the Core scheme of bridge, to improve the load performance, when the game before starting the marketing page, preloaded good native end bridge, in the open business refers to the need to run the front JS code apply colours to a drawing, we also design according to the business scenario design patterns:
- Preloading service packages: Load the complete business package to memory in advance, generate and cache ReactInstanceManager object, in the business startup, from the memory cache to obtain the object, and directly run binding RootView, after transformation, the program can improve the overall open speed of 30%-50% or so, under the game environment, Mobile phone devices are basically turned on in seconds, and simulator devices are within 2s. However, this method of exchange for speed through memory is obviously not desirable after a large volume of business, so the limitation of whole packet preloading is relatively strong.
- Common package preloading: Aiming at the limitations of full package preloading, we proposed a subcontracting scheme to preload Common package. The study found that the business package generated by ReactNative package actually has two parts: one is the Common basic components and API package, collectively called Common package, and the other is the core logic package of the business. Transform packaging, can separate the original full package mode into Common +bussiness, in the multi-service package mode, can share the unified common package, before opening the business, we will give priority to preload common package, and cache the corresponding ReactInstanceManager object, After the user triggers to open the service, the solution loads the Bussiness package. The performance of the solution is slightly worse than that of the full package preloading, but it can improve 15%-20% than that of the non-preloading solution. It also supports multi-service operating environment
- From the perspective of timing, in addition to the initialization of core Bridge, js running to the page display actually takes a lot of time. In the preloading of Core Bridge, we take a step further and support the preloading of RootView, which will be cached in memory before the page rootView is run. Of course, the basic module is still loaded here. When services are opened, the display page can be triggered by routing, which can achieve the page opening without delay. However, the memory cost is higher than that of pre-loading core Bridge.
Of course, the above schemes are based on memory for performance, and different loading methods have achieved cloud control, switching and closing at any time. There are other ways to optimize startup performance in addition to these options:
- Lazy Module: The engine self-defined API Native Module is transformed into Lazy loading mode, and the overall performance is improved by about 5%.
- The business code should require as required, and the part that does not need to be displayed should be adopted lazy require to improve the display and rendering speed of the page.
- Cut service packages, delete modules, apis, and components that do not use React service codes, and reduce the size of service packages to improve startup performance.
- According to the test data, the smaller the service package is, the better the startup performance is. If the package size cannot be reduced, the service package can be divided into sub-packages according to routes, which can also solve the startup speed problem immediately. Service packages are divided into multiple service packages based on routing pages and functions to reduce the size of service logic packages on the first screen. Other service packages can be loaded on demand to improve the startup performance of the home page.
These solutions solve the problem of slow startup performance from the perspective of engine loading, so as to load on demand and optimize the overall performance. However, in the game, the display of the business page is still too dependent on the service request to complete the page rendering, so after the gradual optimization, we found that the network request also accounts for a large part of the page display. In order to further improve the first screen display, we added the network request pre-pull and picture pre-buffer scheme:
- Network prepull: For some network requests that greatly affect the first screen display, after the engine is loaded and the request is obtained from the cloud control platform at an appropriate time, the request is pulled and cached in the memory according to the configuration. After services are started, the network interface content is preferentially read from the cache and displayed.
- Image preloading. For images that are slow to load, the link is configured to the cloud and pre-loaded into Fresco memory when appropriate. When the page opens, Fresco will read the bitmap directly from the cache
In addition to these solutions, replacing the JSC engine to Hermes is a good solution to the startup performance problem, which will be highlighted in the following sections.
2. Memory optimization
All of the above optimization is more aimed at startup performance optimization design, was widely used to improve loading performance of the project, in the complex environment of the game, in addition to performance, also is very strict to the requirement of memory, the game starts, itself for memory consumption is higher than the average native app, so the memory usage would be more accurate and strict, How does ReactNative optimize memory?
- Subcontracting program, subcontracting program in addition to the start speed has a great optimization, load on demand, for memory has also been optimized.
- Font loading, because the game font library cannot be shared with the native font, the use of fonts in the ReactNative page greatly increases the overall memory. In order to reduce the memory of the font, we support the font clipping scheme, insert fonts as needed, delete some rare words, and greatly reduce the size of the font package. In addition, font files also have a great impact on the size of business packages. We support dynamic font delivery and loading.
- Image optimization, in addition to the memory occupied by business UI and JS itself, the larger memory occupation is the image, and the image cache, in order to reduce the image memory consumption, we support webP, GIF and other formats of the image, lossy compression, at the same time for the network image according to the mobile phone resolution. In addition, API is provided to the front-end service to clear unused pictures as needed, release memory in time, and control the size of picture cache.
3. Rendering performance
In addition to memory and startup performance, rendering performance in the game is also crucial. Due to the high memory and CPU load in the game, ReactNative performs worse than native App due to the same page complexity. In order to optimize these indicators, we analyzed and optimized the rendering process of ReactNative, supporting a frame rate of 60fps in the static state. The general optimization is as follows:
- ReactNative is front-end event-driven native UI rendering, so ReactNative is designed to process UI updates in the UI thread at the callback of the Frame Buffer after the drawing of each Frame, even if there is no update, it will run empty, which is in games with high UI thread load. Increases the burden on the UI
- Animation and click events are all designed in the same way, with constant idle tasks occupying the UI thread, increasing the time it takes the UI thread to draw each time
- Solve this problem, is to support resources on-demand loaded, we will animation, the UI map update events on the news, every time a rendering is finished, we will check the map, there is a need to handle the message, no follow-up, no longer in a rendering scheduling after the completion of the UI thread, when a user triggers the animation or UI update, will send a message map, And register the callback rendered by the frame, checking the MAP message in the callback to update the UI
In addition, ReactNative uses native UI rendering. When hardware acceleration is turned on, the overall rendering performance is relatively high. However, in the game environment, most games do not enable hardware acceleration (due to rendering components and engines). The UI is slow to respond, especially in the case of emulators (fPS30 limited), rendering performance is even less than satisfactory. How can you improve performance in complex interactions?
- Simple UI design, no big picture background, no hardware acceleration, overall discharge is not bad, but with a big background, UI performance is especially poor, so to solve the rendering problem, in fact, more to solve the problem of big picture rendering
- ReactNative provides renderToHardwareTextureAndroid to use native memory in rendering performance, the problem is caused by memory consumption is higher, the picture is not too much, is not very strict business memory limit, can use this way to improve performance
- For businesses that use a lot of pictures, we design a set of components that adopt OpengL rendering mode and support texture graph (relatively common ETC1), which has obviously been greatly improved in memory and rendering performance. However, this mode relies on hardware acceleration, so it is generally used in Dialog window mode. The specific implementation principle is as follows: You can pay attention to the author’s article, and will share it with you in detail
Core sample code:
/* GLES20.glCompressedTexImage2D(target, 0, ETC1.ETC1_RGB8_OES , bitmap.getWidth(), bitmap.getHeight(), 0, etc1tex.getData().capacity(), etc1tex.getData()); * /Copy the code