10
头图
If you like my article, I hope to like it 👍 Collection 📁 Comment 💬 Three consecutive support, thank you, this is really important to me!

Preface

previous article mainly analyzed the initialization process of React Native from the Native perspective, and from the source code, summarized several optimization points of React Native container initialization. This article mainly starts with JavaScript and summarizes some optimization points on the JS side.

1.JSEngine

rn_start_jsEngine

Hermes

Hermes is a JS engine open sourced by FaceBook in mid-2019. From the release record, it can be seen that this is a JS engine built for React Native. It can be said that it was built for the Hybrid UI system from the beginning of the design.

Hermes supports direct loading bytecode , that is to say, Babel , Minify , Parse and Compile these processes is all done on the developer's computer, issued a direct bytecode allow Hermes to run on the line, this can omitted JSEngine The process of parsing and compiling JavaScript , the loading speed of JS code will be greatly accelerated, and the startup speed will also be greatly improved.

Hermes

For more information about the features of Hermes, you can read my old article "Which mobile JS engine is strong?" This article, I made a more detailed feature description and data comparison, so I won't say more here.

2.JS Bundle

rn_start_jsBundle

The previous optimizations are actually optimizations of the Native layer. From here on, we have entered the most familiar field of the web front end.

In fact, when it comes to the optimization of JS Bundle, there are several paths back and forth:

  • shrink : reduce the total volume of Bundle, reduce the time of JS loading and parsing
  • extension : dynamic import, lazy loading, on-demand loading, delayed execution
  • dismantle : split public modules and business modules to avoid repeated introduction of public modules

If you have a small partner with webpack packaging optimization experience, see the above optimization methods, do you have some webpack configuration items in your mind? However, the packaging tool of React Native is not webpack, but Facebook’s self-developed Metro . Although the configuration details are different, the principles are the same. Now I will talk about how React Native optimizes JS Bundle.

2.1 Reduce the size of JS Bundle

When Metro packaged JS, will CommonJS ESM module into the module, which leads to more fire now depends on the ESM Tree Shaking totally ineffective, but also according to official reply , Metro will not support future Tree Shaking:

(Tree Shaking 太 low 了,我们做了个更酷的 Hermes)

For this reason, we mainly reduce the volume of the bundle in three directions:

  • For the same function, prefer a smaller third-party library
  • Use the babel plug-in to avoid full references
  • Develop coding standards to reduce duplication of codes

Below we give a few examples to explain the above three ideas.

2.1.0 Use react-native-bundle-visualizer to view package volume

Before optimizing the bundle file, you must know what is in the bundle. The best way is to visually list all dependent packages. In web development, you can use Webpack's webpack-bundle-analyzer plug-in to view the dependency size distribution of the bundle. React Native also has similar tools. You can use react-native-bundle-visualizer view the dependencies:

Xnip2021-04-15_20-36-59

It is very simple to use, just install and analyze according to the document.


2.1.1 moment.js is replaced by day.js

This is a very classic example. It is also a third-party library for time formatting, moment.js volume of 200 KB, day.js volume of only 2KB, and the API is consistent with moment.js. If moment.js is used in the project, replacing it with day.js can immediately reduce the size of JSBundle.


2.1.2 lodah.js with babel-plugin-lodash

Lodash basically belongs to the standard configuration of the Web front-end engineering, but for most people, for the nearly 300 functions encapsulated by lodash, only a few commonly used, such as get , chunk , are used for the full reference of these functions. Somewhat wasteful.

In the community, facing this scenario, there are of course optimization solutions, such as lodash-es , export functions in the form of ESM, and then use the Tree Sharking optimization of tools such as Webpack to keep only the referenced files. As mentioned earlier, however, React Native packaging tools do not support Metro Tree Shaking, so for lodash-es file, in fact, the whole amount will be introduced, and lodash-es full amount of files than lodash much greater.

I did a simple test. For a React Native application that has just been initialized, the package size will increase by 71.23KB after the full introduction of lodash-es , and the package size will increase by 173.85KB after the full introduction of 06079440aee0b3.

Since lodash-es not suitable for use in RN, we can only find a way lodash There is actually another usage of lodash, which is to directly quote a single file. For example join , we can quote it like this:

// 全量
import { join } from 'lodash'

// 单文件引用
import join from 'lodash/join'

In this way, only the file lodash/join

But this is still too troublesome. For example, if we want to use seven or eight methods of lodash, then we have to import seven or eight times separately, which is very cumbersome. For such a popular tool library as babel-plugin-lodash can operate the AST to do the following automatic conversion during JS compilation:

import { join, chunk } from 'lodash'
// ⬇️
import join from 'lodash/join'
import chunk from 'lodash/chunk'

The method of use is also very simple, first run yarn add babel-plugin-lodash -D install, and then enable the plug- babel.config.js

// babel.config.js

module.exports = {
  plugins: ['lodash'],
  presets: ['module:metro-react-native-babel-preset'],
};

I take the join method as an example. You can take a look at each method increase the JS Bundle volume of

Full lodashFull loads-eslodash/join single file referencelodash + babel-plugin-lodash
71.23 KB173.85 KB119 Bytes119 Bytes

It can be seen from the table that lodash with babel-plugin-lodash is the best development option.


2.1.3 Use of babel-plugin-import

babel-plugin-lodash can only convert lodash . In fact, there is a very useful babel plugin in the community: babel-plugin-import , Basically it can solve all on-demand quoting problems .

Let me give a simple example. Ali has a very useful ahooks open source library, which encapsulates many commonly used React hooks, but the problem is that this library is encapsulated for the Web platform. For example, useTitle is used to set up The title of the page, but the React Native platform does not have a related BOM API, so there is no need to introduce this hook at all, and RN will never use this API.

At this time, we can use babel-plugin-import implement on-demand reference. Assuming that we only useInterval the Hooks of 06079440aee23f, we now introduce in the business code:

import { useInterval } from 'ahooks'

Then run yarn add babel-plugin-import -D install the plug-in, and enable the plug- babel.config.js

// babel.config.js

module.exports = {
  plugins: [
    [
      'import',
      {
        libraryName: 'ahooks',
        camel2DashComponentName: false, // 是否需要驼峰转短线
        camel2UnderlineComponentName: false, // 是否需要驼峰转下划线
      },
    ],
  ],
  presets: ['module:metro-react-native-babel-preset'],
};

After enabling, the on-demand introduction of ahooks can be realized:

import { useInterval } from 'ahooks'
// ⬇️
import useInterval from 'ahooks/lib/useInterval'

The following is the volume increment of in various situations. In general, babel-plugin-import is the best choice:

Full ahooksahooks/lib/useInterval single file referenceahooks + babel-plugin-import
111.41 KiB443 Bytes443 Bytes

Of course, babel-plugin-import can act on many library files, such as internal/third-party packaged UI components, which can basically be introduced on demand through the configuration items of babel-plugin-import If there is a need, you can see the experience summarized by other people on the Internet, I won't say more here.


2.1.4 babel-plugin-transform-remove-console

It is also very useful to remove the console's babel plug-in. We can configure it to remove the console statement when it is packaged and released. This reduces the package size and speeds up the JS running speed. We only need to simply configure it after installation:

// babel.config.js

module.exports = {
    presets: ['module:metro-react-native-babel-preset'],
    env: {
        production: {
            plugins: ['transform-remove-console'],
        },
    },
};


2.1.5 Develop good coding standards

There are too many best practices for coding standards. In order to fit the theme (reduce code size), I will just cite a few points:

  • Code abstraction and reuse : The repeated logic in the code should be abstracted into a method as far as possible according to the degree of reusability, instead of copying once
  • delete invalid logic : This is also very common. With the iteration of the business, a lot of code will not be used. If a function goes offline, it will be deleted directly. Whenever you need to use it, you will find it in the git record.
  • delete redundant styles : For example, introduce ESLint plugin for React Native , turn on the "react-native/no-unused-styles" option, and use ESLint to prompt invalid style files

To be honest, these few optimizations can't actually reduce a few KB of code. The greater value is improve the robustness and maintainability of the project .

2.2 Inline Requires

Inline Requires can be understood as lazy execution , note that I say here is not lazy loading, because under normal circumstances, after initialization RN container full amount of load files parsing JS Bundle, Inline Requires role is delay run , meaning that only need to use JS code will only be executed when it is started, not when it is started. In React Native 0.64 version, Inline Requires enabled by default.

First, we want to metro.config.js confirmed in turn the Inline Requires features:

// metro.config.js

module.exports = {
  transformer: {
    getTransformOptions: async () => ({
      transform: {
        experimentalImportSupport: false,
        inlineRequires: true, // <-- here
      },
    }),
  },
};

In fact Inline Requires is very simple, which is to change the import position of require.

For example, we wrote a utility function join in the file utils.js

// utils.js

export function join(list, j) {
  return list.join(j);
}

Then we import this library App.js

// App.js

import { join } from 'my-module';

const App = (props) => {
  const result = join(['a', 'b', 'c'], '~');

  return <Text>{result}</Text>;
};

The above wording, after Metro equivalent to compiled into the following:

const App = (props) => {
  const result = require('./utils').join(['a', 'b', 'c'], '~');

  return <Text>{result}</Text>;
};

The actual compiled code actually looks like this:

rn_start_inlineRequire

The red line in FIG r() function, in fact, RN own package require() function, it can be seen Metro automatically import moved to the top position of use.

It is worth noting that Metro’s automatic Inline Requires configuration, currently does not support export default exports, that is, if your join function is written like this:

export default function join(list, j) {
  return list.join(j);
}

It looks like this when importing:

import join from './utils';

const App = (props) => {
  const result = join(['a', 'b', 'c'], '~');

  return <Text>{result}</Text>;
};

Metro compiled the converted code, the corresponding import is still at the top level of the function :

rn_start_require

This requires special attention. The community also has related articles, urging everyone not to use export default who are interested can find out:

in-depth analysis of ES Module (1): disable export default object

in-depth analysis of ES Module (two): completely disable default export

2.3 JSBundle sub-package loading

Subcontracting scenes generally appear in scenes where Native is the mainstay and React Native is the secondary. This scenario is often like this:

  • Suppose there are two RN-based business lines A and B, and their JSBundles are all dynamically issued
  • A's JSBundle is 700KB in size, including 600KB of basic package (React, React Native JS code) and 100KB of business code
  • The size of JSBundle of A is 800KB, including 600KB of basic package and 200KB of business code
  • Each time you jump from Native to the A/B RN container, you must full run the analysis

As you can see from the above example, the 600KB basic package is repeated in multiple business lines. There is no need to download and load multiple times. At this time, an idea comes out naturally:

There are some libraries packaged into a common.bundle file, as long as we have each issued a dynamic business package businessA.bundle and businessB.bundle , then realized to load the client common.bundle file, then loaded business.bundle files on it

There are several advantages of this:

  • common.bundle can be placed directly locally, eliminating multiple downloads for multiple business lines, saves traffic and bandwidth
  • common.bundle when the RN container is pre-initialized, and has a smaller size and faster initialization speed

Following the above thoughts, the above problem will be transformed into two small problems:

  • How to realize the unpacking of JSBundle?
  • How does the RN container of iOS/Android implement multi-bundle loading?


2.3.1 Unpacking JS Bundle

Before unpacking, you must first understand the workflow of Metro, a packaging tool. Metro's packaging process is very simple, with only three steps:

  • Resolution : It can be simply understood as analyzing the dependencies of each module, and finally a dependency graph will be generated
  • Transformation : code compilation and transformation, mainly with the help of Babel's compilation and transformation capabilities
  • Serialization : After all codes are converted, print the converted codes to generate one or more bundle files

As can be seen from the above process, our unpacking step will only be at Serialization . We only need to use the Serialization exposed by 06079440aee814 to achieve bundle subcontracting.

Before the formal subcontracting, let us put aside various technical details and simplify the problem: For an array of all numbers, how to divide it into an even array and an odd array?

This question is too simple. Anyone who is just learning programming should be able to think of the answer. Iterate through the original array. If the current element is odd, put it in the odd array, if it is even, put it in the even array.

Metro's subcontracting of JS bundles is actually a reason. When Metro is packaging, it will set the moduleId for each module. This id is a self-increasing number starting from 0. When we subcontract, public modules (such as react react-native ) are output to common.bundle , and business modules are output to business.bundle .

Because of the need to take into account multiple business lines, the mainstream subcontracting scheme in the industry now looks like this:

1. first create a common.js file, which introduces all public modules, and then Metro uses this common.js as the entry file to type a common.bundle file, also records the moduleId of all public modules

// common.js

require('react');
require('react-native');
......

2. business line A, and the package entry file of Metro is the project entry file of A. During the packaging process, filter public module moduleId recorded in the previous step, so that the result of the packaging is only the business code of A

// indexA.js

import {AppRegistry} from 'react-native';
import BusinessA from './BusinessA';
import {name as appName} from './app.json';

AppRegistry.registerComponent(appName, () => BusinessA);

3. Business Line BCD E...... The packaging process is the same as Business Line A


The above idea looks very good, but there is still a problem: every time Metro packaging is started, the moduleId is incremented from 0, which will cause different JSBundle IDs to repeat .

In order to avoid id duplication, the current mainstream approach in the industry is to treat the path of the module as the moduleId (because the path of the module is basically fixed and non-conflicting), which solves the problem of id conflicts. Metro exposes the createModuleIdFactory , we can overwrite the original self-increment number logic in this function:

module.exports = {
  serializer: {
    createModuleIdFactory: function () {
      return function (path) {
        // 根据文件的相对路径构建 ModuleId
        const projectRootPath = __dirname;
        let moduleId = path.substr(projectRootPath.length + 1);
        return moduleId;
      };
    },
  },
};


Integrating the ideas of the first step, you can build the following metro.common.config.js configuration file:

// metro.common.config.js

const fs = require('fs');

module.exports = {
  transformer: {
    getTransformOptions: async () => ({
      transform: {
        experimentalImportSupport: false,
        inlineRequires: true,
      },
    }),
  },
  serializer: {
    createModuleIdFactory: function () {
      return function (path) {
        // 根据文件的相对路径构建 ModuleId
        const projectRootPath = __dirname;
        let moduleId = path.substr(projectRootPath.length + 1);
        
        // 把 moduleId 写入 idList.txt 文件,记录公有模块 id
        fs.appendFileSync('./idList.txt', `${moduleId}\n`);
        return moduleId;
      };
    },
  },
};

Then run the command line command to package:

# 打包平台:android
# 打包配置文件:metro.common.config.js
# 打包入口文件:common.js
# 输出路径:bundle/common.android.bundle

npx react-native bundle --platform android --config metro.common.config.js --dev false --entry-file common.js --bundle-output bundle/common.android.bundle

Through the packaging of the above commands, we can see that the moduleId is converted to a relative path, and idList.txt also records all the moduleIds:

common.android.bundle

idList.js


The key to the second step is to filter the moduleId of the public module. Metro provides the processModuleFilter , which can be used to filter the module. The specific logic can be seen in the following code:

// metro.business.config.js

const fs = require('fs');

// 读取 idList.txt,转换为数组
const idList = fs.readFileSync('./idList.txt', 'utf8').toString().split('\n');

function createModuleId(path) {
  const projectRootPath = __dirname;
  let moduleId = path.substr(projectRootPath.length + 1);
  return moduleId;
}

module.exports = {
  transformer: {
    getTransformOptions: async () => ({
      transform: {
        experimentalImportSupport: false,
        inlineRequires: true,
      },
    }),
  },
  serializer: {
    createModuleIdFactory: function () {
      // createModuleId 的逻辑和 metro.common.config.js 完全一样
      return createModuleId;
    },
    processModuleFilter: function (modules) {
      const mouduleId = createModuleId(modules.path);
      
      // 通过 mouduleId 过滤在 common.bundle 里的数据
      if (idList.indexOf(mouduleId) < 0) {
        console.log('createModuleIdFactory path', mouduleId);
        return true;
      }
      return false;
    },
  },
};

Finally, run the command line command to package:

# 打包平台:android
# 打包配置文件:metro.business.config.js
# 打包入口文件:index.js
# 输出路径:bundle/business.android.bundle

npx react-native bundle --platform android --config metro.business.config.js --dev false --entry-file index.js --bundle-output bundle/business.android.bundle

The final packaging result is only 11 lines (398 lines without subcontracting), which shows that the benefits of subcontracting are very large.

business.android.bundle

Of course, when the relative path is used as the moduleId for packaging, it will inevitably lead to a larger package. We can use md5 to calculate the relative path, and then take the first few digits as the final moduleId; or use incremental id, but use more complicated Mapping algorithm to ensure the uniqueness and stability of moduleId. The content of this part actually belongs to the very classic Map key design problem . Interested readers can learn about the theory of related algorithms.


2.3.2 Native implements multi-bundle loading

Subcontracting is only the first step. If you want to show a complete and correct RN interface, you also need to "combine". This "combination" means to load multiple bundles on the Native side.

It is easier to load common.bundle, just load it directly when the RN container is initialized. I have introduced the process of container initialization in detail in the previous section, so I won’t talk about it here. At this time, the problem is converted to the loading problem business.bundle

React Native is not like the browser's multi-bundle loading. It can directly generate a <script /> tag and insert it into the HTML to achieve dynamic loading. We need to combine specific RN container implementations to achieve the loading requirements of business.bundle At this time we need to pay attention to two points:

  1. Timing : When does the loading start?
  2. Method : How to load a new bundle?


For the first question, our answer is common.bundle then finished loading business.bundle .

common.bundle loaded, the iOS side will send RCTJavaScriptDidLoadNotification , and the Android side will call back the onReactContextInitialized() method to all ReactInstanceEventListener registered in the ReactInstanceManager instance. We can load the business package in the corresponding event listener and callback.


For the second question, for iOS, we can use the executeSourceCode method of RCTCxxBridge to execute a piece of JS code in the context of the current RN instance to achieve the purpose of incremental loading. However, it is worth noting that executeSourceCode is a private method of RCTCxxBridge, we need to use Category to expose it.

The Android side can use the newly created ReactInstanceManager instance to obtain the current ReactContext context object getCurrentReactContext() getCatalystInstance() method of the context object to obtain the media instance, and finally call the loadScriptFromFile(String fileName, String sourceURL, boolean loadSynchronously) method of the media instance to complete the incremental loading of the business JSBundle.

The sample codes for iOS and Android are as follows:

NSURL *businessBundleURI = // 业务包 URI
NSError *error = nil;
NSData *sourceData = [NSData dataWithContentsOfURL:businessBundleURI options:NSDataReadingMappedIfSafe error:&error];
if (error) { return }
[bridge.batchedBridge executeSourceCode:sourceData sync:NO]
ReactContext context = RNHost.getReactInstanceManager().getCurrentReactContext();
CatalystInstance catalyst = context.getCatalystInstance();
String fileName = "businessBundleURI"
catalyst.loadScriptFromFile(fileName, fileName, false);



The sample codes in this section are all at the demo level. If you want to truly connect to the production environment, you need to customize it based on the actual architecture and business scenarios. There is a React Native subcontracting warehouse react-native-multibundler content is quite good, you can refer to it for learning.

3.Network

rn_start_network

componentDidMount() React Component is executed, obtain data from the server, and then change the state of the Component to render the data.

Network optimization is a very large and independent topic. There are many points to optimize. Here are a few network optimization points related to first screen loading:

  • DNS cache : Cache the IP address in advance, skip the DNS addressing time
  • cache reuse : Before entering the RN page, request network data in advance and cache it. After opening the RN page, check the cache data before requesting the network. If the cache has not expired, get the data directly from the local cache
  • Request to merge : If you are still using HTTP/1.1, if there are multiple requests on the first screen, you can merge multiple requests into one request
  • HTTP2 : Use HTTP2 parallel requests and multiplexing to optimize speed
  • Reduce the volume : Remove redundant fields of the interface, reduce the volume of image resources, etc.
  • ......

Since the network is relatively independent here, the optimization experience of iOS/Android/Web can actually be used on RN, and it is enough to follow everyone's previous optimization experience.

4.Render

rn_start_render

The time-consuming rendering here is basically positively related to the UI complexity of the first screen page . You can use the rendering process to see where time-consuming occurs:

  • VDOM calculation : The higher the page complexity, the longer the calculation time on the JavaScript side (VDOM generation and Diff)
  • JS Native communication : JS calculation results will be converted to JSON and passed to the Native side through Bridge. The higher the complexity, the larger the data volume of JSON, which may block Bridge communication.
  • Native rendering : Native side recursive analysis of the render tree, the more complex the layout, the longer the rendering time

MessageQueue monitoring in the code to see what is on the JS Bridge after the APP is started:

// index.js

import MessageQueue from 'react-native/Libraries/BatchedBridge/MessageQueue'
MessageQueue.spy(true);

rn_start_MessageQueue

From the picture, we can see that after the JS is loaded, there are a large number of UI-related UIManager.createView() UIManager.setChildren() . Combined with the above time-consuming summary, we have several solutions:

  • Reduce the UI nesting level through certain layout techniques and reduce the complexity of UI views
  • Reduce re-render, directly intercept the redraw process on the JS side, reduce the frequency of bridge communication and the amount of data
  • If it is an APP with React Native as the main architecture, the first screen can be directly replaced with Native View, which is directly separated from the rendering process of RN

The above techniques are explained in in the old article 16079440aeee3b "React Native Performance Optimization Guide-Rendering" , so I won't explain more here.

Fraic

From the above, we can see that the rendering of React Native needs to transmit a large amount of JSON data on the Bridge. When React Native is initialized, the amount of data will block the bridge and slow down our startup and rendering speed. Fraic in React Native's new architecture can solve this problem. JS and Native UI are no longer asynchronous communication, and can be directly called, which can greatly accelerate rendering performance.

Fraic can be said to be the most anticipated in the new RN architecture. If you want to know more, you can go to official issues area to watch.

to sum up

This article mainly JavaScript , summarizes and analyzes various optimization methods of JSBundle, and combines network and rendering optimization to improve the startup speed of React Native applications in an all-round way.

reference

⚡️ React Native startup speed optimization-Native articles (including source code analysis)

🎨 React Native Performance Optimization Guide-Rendering

🤔 Which mobile JS engine is better?

China Merchants Securities react-native hot update optimization practice

How to implement unpacking in React Native?


If you like my article, I hope to like it 👍 Collection 📁 Comment 💬 Three consecutive support, thank you, this is really important to me!

Welcome everyone to pay attention to my WeChat public account: Halogen Lab, currently focusing on front-end technology, and also doing some small research on graphics.

Original link 👉 ⚡️ React Native startup speed optimization-JS chapter : More timely updates, better reading experience


卤代烃
1.6k 声望5.9k 粉丝