The metro-code-split tool in the article is open source, and supports RN unpacking and dynamic import. Welcome everyone to start.
https://github.com/wuba/metro-code-split
The following is the text.
The topic I shared with you today is "58RN page second opening plan and practice". Let me introduce myself first, my name is Jiang Hongwei. I joined 58 in 2015 and began to explore in the RN direction in 2016. Over the past few years, I have also promoted the implementation of many RN performance solutions. In the process of landing, a frequently asked question is:
Doing performance optimization will reduce the time consumption, but what are the benefits for the business?
For the first time, I was indeed asked. With this question, I did an experiment to count the relationship between the first screen time and the visit churn rate. I found an interesting pattern.
For every 1 s reduction in the time to the first screen, the visit churn rate is reduced by 6.9%.
Looking back on the forecast, is the actual effect really good at 6.9%?
We took 6.9% of revenue data to promote the implementation of several businesses. On the whole, the predicted churn rate benefit is actually similar to the actual churn rate benefit, but there are differences. Specifically, pages with good performance are worse than expected returns, and pages with poor performance are better than expected returns. This is also easy to understand. The churn rate of pages with good performance is already very low, and there is little room for further optimization.
When we know that the benefits of performance optimization will diverge, we naturally focus more on the pages that have not yet achieved seconds. We have designed several indicators, including churn rate, first screen time, and second-to-open revenue.
Churn rate and time to first screen are post-event indicators. The second opening profit indicator is an ex-ante indicator. It will tell you how much your churn rate will be reduced if your page achieves second opening? We hope to use this series of indicators to drive business performance optimization. At the same time, we will also provide some low-cost or even no-cost optimization solutions for the business to help the business save and optimize costs.
Indicators drive business, business selection plans, and plans to increase revenue. This is the revenue-driven model we envision.
First screen time collection scheme
This sharing will also focus on the two areas of the plan and indicators for specific development. Let's talk about the indicators first. The most important indicator is the first screen time. After the first screen time is calculated, the churn rate and the second-time revenue are actually calculated. Therefore, this sharing is divided into the following three parts.
- The first part is about the acquisition plan of the first screen time
- The second part will talk more specifically about the performance optimization program
- Finally, I will summarize and look forward to everyone.
Let's first look at the loading process of a page, which has about 5 stages:
- 0 ms: user enters
- 410 ms: First content drawing FCP
- 668 ms: Business component DidUpdate
- 784 ms: Maximum content drawing LCP
- 928 ms: The visual area has been loaded
The second, third, fourth, and fifth time points can all be defined as the first screen time. The definition of the first screen time is different, the time consuming is also different, and the gap is very large. Therefore, we need to select an indicator first as the definition of time above the fold.
The first screen time we choose is the LCP indicator. why?
- First, because LCP is the largest content drawing, the main elements on the page have actually been displayed at this time.
- Secondly, because LCP can realize non-intrusive collection, there is no need for business to manually bury points.
- Third, because the LCP is a W3C draft, this is an important reason. You tell others that your first screen indicator is LCP, and they will understand it, so there is no need to explain too much.
In order to allow everyone to better understand the implementation of the LCP algorithm, let's pave the way for everyone.
Simply put, LCP is the largest element you can see, and the time it takes to render. But there is a problem here. For example, the largest element of our second picture and the largest element of the fifth picture are not the same element. Different elements have different rendering time, and LCP is also different. In other words, there are multiple LCPs on a page, which LCP should be reported? The final converged LCP value should be reported, that is, the LCP value when the visual area is loaded.
LCP is a Web standard, and it is not implemented in RN. How should it be implemented?
On the whole, the realization is roughly divided into 5 steps:
- When the user enters, the Start timestamp is recorded by the Native thread.
- The Start timestamp is injected into the JS Context by the Native thread.
- The JS thread listens to the layout events of the rendered elements on the page.
- The JS thread performs calculations during the page rendering process and continuously updates the LCP value.
- The JS thread calculates the End timestamp and reports the final LCP value.
At this time, the final reported LCP = End Time-Start Time.
The difficulty is how to converge the LCP, that is, how to determine that the viewing area is fully loaded. The rule we adopt is that when all elements are loaded and the bottom element has also been loaded, the visual area is loaded. The element has a call cycle, call render first, then call layout. Only elements that call render are elements that have not been loaded. The element that has called render and called layout is the element that has been loaded. Being able to judge whether an element has been loaded is also able to judge whether the visual area has been loaded.
Performance optimization program
Before talking about the specific plan, let's talk about the overall idea of our performance optimization.
Before doing any performance optimization, we must first analyze what the performance structure is, then find the performance bottleneck, and come up with a specific optimization plan based on the bottleneck.
The performance structure of an RN application, as a whole, is divided into two parts, the Native part and the JS part. To be more specific, it can be divided into 6 parts. The following is the time-consuming structure of an unoptimized, more complex, and dynamically updated RN application:
- Version request 200 ms
- Resource download 470 ms
- Native initialization 350 ms
- JS initialization 380 ms
- Business request 420 ms
- Business rendering 460 ms
Generally speaking, the above 6 structures can be divided into 3 bottlenecks.
- Dynamic update bottleneck, accounting for 29%.
- Initialize the bottleneck, accounting for 32%.
- Business time-consuming bottlenecks, accounting for 39%.
Bottleneck one: dynamic update
One of the characteristics of Internet products is rapid trial and error, which requires fast iteration of the business. In order to support rapid business iteration, this requires applications to be dynamically updated. For dynamic updates, a request must be sent, and it will slow down performance if a request is sent, such as the Web. If, like Native, built-in resources, the performance will be much better, but how to update it dynamically?
Dynamic update and performance seem to be a contradiction. Is there any trade-off?
The solution we began to think of was to improve page performance through built-in resources and dynamically update through silent updates.
When the user first comes in, because there are already built-in resources, there will be no request and the page can be rendered directly. At the same time, the Native thread will update silently in parallel, asking whether the server has the latest version, and if there is, it will download the bundle and update the cache. In this way, when the user next comes in, they can use the resources cached last time, directly render the page, and simultaneously update silently in parallel. By analogy, every time the user enters, there is no request and the page can be rendered directly.
There is a small detail that needs attention when designing silent updates. The user uses the last cached resource each time, not the latest resource online. Therefore, there is a risk that a version with serious bugs is cached by the user and cannot be updated. To this end, we have designed a mandatory update function. After the silent update is successful, the Native thread informs the JS thread, and the business decides whether to force the update to the latest version according to the specific situation.
The built-in resource + silent update solution also has some disadvantages:
- Increase the size of the App. For super apps, the volume is already very large, and it is difficult to increase the volume.
- The coverage of the new version is low. The coverage rate of the new version in 72 hours is about 60%, which is relatively low compared to the Web solution.
- The version fragmentation is serious. Multiple built-in versions and multiple dynamic updates will cause version fragmentation and drive up maintenance costs.
Therefore, we have made some improvements.
Use resource pre-loading instead of resource built-in. This largely avoids the problems of package volume, coverage, and fragmentation. The silent update is still retained, to update the possible BUG version.
The topic of resource preloading is actually bad. I will only analyze it from the perspective of "rights".
Who should have the right to preload? Is it the RN framework or the specific business? Give permissions to the framework. The framework can preload the resources of all pages, but this is obviously very inefficient. For platform-level apps, an app has dozens or even hundreds of RN apps, most of which are preloaded The resources of users are not available, which results in waste. It is very troublesome to give permissions to businesses and load specific businesses one by one.
Information is right. Whoever owns the information will give the right. At the beginning, the framework did not have any useful information, but the business can know the proportion of jumping to a specific page based on the business data, so the right to call the preload should be given to the business. When the user has used a certain RN application, the framework knows this information, and the right should be given to the framework at this time. The framework can make a version pre-request after the App is started.
Aiming at the dynamic update bottleneck, we used the solution of resource preloading and silent update. The time-consuming 2280 ms, which was never optimized, was reduced to 1610 ms, a 29% decrease.
Bottleneck 2: Framework initialization bottleneck
First, let's analyze why the frame initialization is slow.
JS thread and Native thread communicate asynchronously, and each communication is serialized and deserialized through Bridge. Before the communication, because they are not in a Context, the JS thread and the Native thread do not know each other's existence. Because Native does not know which NativeModule JS will use, Native needs to initialize all NativeModules instead of on-demand initialization, which is the reason for the slow initialization performance.
In the new RN architecture, there are plans to replace asynchronous Bridge communication with synchronous JSI communication to achieve on-demand initialization. But the on-demand initialization function has not yet been implemented, so we still need to optimize the framework initialization.
The idea we give is, unpacking built-in and framework pre-execution.
Our App is a hybrid application, and the homepage is not RN. Therefore, after the App is started, the RN built-in package can be executed first to initialize all NativeModules. When the user actually enters the RN page, the performance will naturally increase much faster.
The biggest difficulty of this program is unpacking. How to correctly disassemble a complete bundle into built-in packages and dynamic update packages?
At first we stepped on a pit, hoping to help everyone avoid it.
It turns out that we used Google’s diff-match-patch algorithm, which compares the difference between the new and the old text and generates a patch file. Similarly, you can use the diff-match-patch algorithm to compare the difference between the service package and the built-in package to generate a patch dynamic update package.
However, patch is actually a "text patch", and "text patch" cannot be executed separately. It cannot meet the requirement of executing the built-in package first and then executing the dynamic update package.
Later, we modified the metro to realize the correct unpacking, thus realizing the pre-loading of the framework.
A complete bundle consists of several modules. How to distinguish whether a module belongs to a built-in package or a dynamically updated package? The path or ID of the built-in module has a feature, which is under the path of node_modules/react/xxx or node_modules/react-native/xxx. You can record the IDs of all built-in modules in advance, and filter out all built-in modules when packaging, and generate a dynamic update package that only contains business modules.
The dynamic update package unpacked by metro is a "code patch", which can be executed directly, which can meet the requirements of executing the built-in package first, and then the dynamic update package.
One of the details is that a line of require(InitializeCore) should be added to the built-in package to call the modules defined in the built-in package. By adding this line of code, the first screen time can be reduced by about 90 ms.
For the framework initialization bottleneck, we used the unpacking built-in and framework pre-execution scheme. The time-consuming 1610 ms, which has never been optimized, has been reduced to 1300 ms, an overall decrease of 43%.
Bottleneck three: business request bottleneck
After the dynamic update bottleneck and framework time-consuming bottleneck are optimized, let's look at the business bottleneck again. The business bottleneck is mainly composed of two parts: business request and business rendering. The request is better optimized, so we first optimize the business request bottleneck.
There are actually many common solutions for the optimization of business requests.
- Business data cache
- Preload the business data of the next page on the previous page
However, not every application is suitable for caching, and not every application’s data is suitable for preloading on the previous page. Therefore, we need a more general solution. Take a closer look. The Init part and the business request part are serial. Can they be changed to parallel?
Our idea is to replace JS by Native and request business data directly and in parallel when the user enters the page.
The specific plan is as follows.
- The resource file downloaded by Native will contain both the Biz business package and the URL of the original business request.
- The original URL will contain dynamic business parameters, and the variables will be converted according to pre-agreed rules. For example,
58.com/api?user=${user}
will be converted to58.com/api?user=GTMC
. - Native executes Biz package rendering pages in parallel, and initiates URL requests to obtain business data.
- The JS side directly calls PreFetch(cb) to obtain the data requested by the Native side.
Aiming at the bottleneck of business requests, we use the parallel loading of business data. The time-consuming 1300 ms, which has never been optimized, has been reduced to 985 ms, an overall decrease of 57%.
With the above solution, most pages can be opened in seconds. Is there room for performance optimization?
Code execution bottleneck
Another reason for the slow rendering of the RN page is that RN needs to execute a complete JS file, even if there is code that does not need to be executed in the JS.
Let's look at a case. A page contains 3 tabs, and users will only see 1 tab when they come in. In theory, only one tab code needs to be executed. But in fact, the code of the other two invisible tabs will also be downloaded and executed, slowing down the performance.
RN code lazy loading and lazy execution capabilities to improve performance, similar to Dynamic Import in the Web.
RN officially did not provide dynamic import, so we decided to do it ourselves.
Currently, the dynamic import demo has run through in RN 0.64 version. When the business is initialized, only the Biz business package can be executed, and the corresponding chunk dynamic package will be dynamically downloaded when jumping to the Foo and Bar dynamic pages. Exit the entered dynamic page and enter it again. The original cache will be used to directly render the dynamic page.
For the dynamic import implementation of RN, we refer to the TC39 specification.
The business only needs to write a line of code import("./Foo")
to achieve lazy loading and lazy execution of the code. All the remaining work is done at the framework layer and platform layer.
When the runtime is running, import("./Foo")
, the framework layer will determine whether the module corresponding to the ./Foo
If there is no install, it will ./Foo
path, then download and execute the chunk, and finally render the Foo Component.
The URL of the Chunk package is a CDN address. Obviously, the work of uploading the CDN and recording the relationship between Path and URL is not done at runtime, but at compile time.
During the compilation process of the platform layer, the relationship table between Path and URL will be stored in the Biz package, so that Runtime can find the corresponding URL through Path.
This process is roughly divided into 5 parts.
- Project: A project consists of several files, and there will be interdependencies between the files.
- Graph: Each file will generate a corresponding module, and all modules and their dependencies form a graph.
- Modules: "color" the collection of dynamic modules to distinguish them.
- Bundles: The collection of multiple modules are packaged into multiple bundles.
- CND: Upload the bundle to the CDN.
The most critical step is to color the collection of dynamic modules.
- Decomposition and coloring: The coloring of a Graph can be decomposed into several basic cases, and the coloring scheme of these basic cases has been determined.
- Dynamic map: After the coloring is completed, the root paths of the "green" and "blue" dynamic modules will be recorded, and a dynamic map will be formed with the CDN URL address of the bundle.
- Path to URL: Dynamic map will be packaged into the "white" Biz business package, so when calling
import()
at runtime, the corresponding URL can be found through Path.
Many of the above details have not been discussed. Students who are concerned about implementation details can pay attention to our open source tool metro-code-split.
metro-code-split:https://github.com/wuba/metro-code-split
- Based on metro
- Support DLL unpacking
- Support RN Dynamic Import
Summary and outlook
By analyzing the performance structure, we found three types of performance bottlenecks and produced different optimization solutions. The figure below is a collection of our Miaokai solutions. The figure lists the (expected) benefits, effective scope and effective scenarios. I hope it will be helpful to everyone's technology selection.
In the latest version, many functions of the new RN architecture have matured, and we are also actively exploring. One of the most surprising is the Hermes engine, which can already be used in both iOS and Android. The biggest difference between the Hermes engine and the original JSCore engine is that Hermes will pre-compile and compile the JS file into a bytecode file during compilation, so that the bytecode file can be directly used for execution at runtime, which can greatly reduce the JS execution cost. Time. After testing, we found that a page that takes 140 ms can be reduced to 40 ms, a drop of 80%.
While we provide performance optimization solutions for the business, we also need to pay attention to the implementation of the business. In order to enable more businesses to be opened in seconds, we collected indicators such as churn rate, time to first screen, and revenue in seconds through non-intrusive collection methods. In our practice, this way of linking technological optimization needs with business benefits is easier to be accepted by the business and easier to promote.
Finally, I hope that our second opening program and revenue-driven practice can inspire you, thank you all.
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。