?? React Native 啟動(dòng)速度優(yōu)化——JS 篇【全網(wǎng)最全,值得收藏】

如果你喜歡我的文章,希望點(diǎn)贊?? 收藏 ?? 評(píng)論 ?? 三連支持一下,謝謝你,這對(duì)我真的很重要!

前言

上一篇文章主要從 Native 的角度分析了 React Native 的初始化流程,并從源碼出發(fā),總結(jié)了幾個(gè) React Native 容器初始化的優(yōu)化點(diǎn)。本文主要從 JavaScript 入手,總結(jié)了一些 JS 側(cè)的優(yōu)化要點(diǎn)。

1.JSEngine

rn_start_jsEngine

Hermes

Hermes 是 FaceBook 2019 年中旬開源的一款 JS 引擎,從 release 記錄可以看出,這個(gè)是專為 React Native 打造的 JS 引擎,可以說從設(shè)計(jì)之初就是為 Hybrid UI 系統(tǒng)打造。

Hermes 支持直接加載字節(jié)碼,也就是說,Babel、Minify、ParseCompile 這些流程全部都在開發(fā)者電腦上完成,直接下發(fā)字節(jié)碼讓 Hermes 運(yùn)行就行,這樣做可以省去 JSEngine 解析編譯 JavaScript 的流程,JS 代碼的加載速度將會(huì)大大加快,啟動(dòng)速度也會(huì)有非常大的提升。

Hermes

更多關(guān)于 Hermes 的特性,大家可以看我的舊文《移動(dòng)端 JS 引擎哪家強(qiáng)》這篇文章,我做了更為詳細(xì)的特性說明與數(shù)據(jù)對(duì)比,這里就不多說了。

2.JS Bundle

rn_start_jsBundle

前面的優(yōu)化其實(shí)都是 Native 層的優(yōu)化,從這里開始就進(jìn)入 Web 前端最熟悉的領(lǐng)域了。

其實(shí)談到 JS Bundle 的優(yōu)化,來來回回就是那么幾條路:

  • :縮小 Bundle 的總體積,減少 JS 加載和解析的時(shí)間
  • :動(dòng)態(tài)導(dǎo)入(dynamic import),懶加載,按需加載,延遲執(zhí)行
  • :拆分公共模塊和業(yè)務(wù)模塊,避免公共模塊重復(fù)引入

如果有 webpack 打包優(yōu)化經(jīng)驗(yàn)的小伙伴,看到上面的優(yōu)化方式,是不是腦海中已經(jīng)浮現(xiàn)出 webpack 的一些配置項(xiàng)了?不過 React Native 的打包工具不是 webpack 而是 Facebook 自研的 Metro,雖然配置細(xì)節(jié)不一樣,但道理是相通的,下面我就這幾個(gè)點(diǎn)講講 React Native 如何優(yōu)化 JS Bundle。

2.1 減小 JS Bundle 體積

Metro 打包 JS 時(shí),會(huì)把 ESM 模塊轉(zhuǎn)為 CommonJS 模塊,這就導(dǎo)致現(xiàn)在比較火的依賴于 ESM 的 Tree Shaking 完全不起作用,而且根據(jù)官方回復(fù),Metro 未來也不會(huì)支持 Tree Shaking :

(Tree Shaking 太 low 了,我們做了個(gè)更酷的 Hermes)

因?yàn)檫@個(gè)原因,我們減小 bundle 體積主要是三個(gè)方向:

  • 對(duì)于同樣的功能,優(yōu)先選擇體積更小的第三方庫
  • 利用 babel 插件,避免全量引用
  • 制定編碼規(guī)范,減少重復(fù)代碼

下面我們舉幾個(gè)例子來解釋上面的三個(gè)思路。

2.1.0 使用 react-native-bundle-visualizer 查看包體積

優(yōu)化 bundle 文件前,一定要知道 bundle 里有些什么,最好的方式就是用可視化的方式把所有的依賴包列出來。web 開發(fā)中,可以借助 Webpack 的 webpack-bundle-analyzer 插件查看 bundle 的依賴大小分布,React Native 也有類似的工具,可以借助 react-native-bundle-visualizer 查看依賴關(guān)系:

Xnip2021-04-15_20-36-59

使用非常簡(jiǎn)單,按照文檔安裝分析就可。

<br />

2.1.1 moment.js 替換為 day.js

這是一個(gè)非常經(jīng)典的例子。同樣是時(shí)間格式化的第三方庫, moment.js 體積 200 KB,day.js 體積只有 2KB,而且 API 與 moment.js 保持一致。如果項(xiàng)目里用了 moment.js,替換為 day.js 后可以立馬減少 JSBundle 的體積。

<br />

2.1.2 lodah.js 配合 babel-plugin-lodash

lodash 基本上屬于 Web 前端的工程標(biāo)配了,但是對(duì)于大多數(shù)人來說,對(duì)于 lodash 封裝的近 300 個(gè)函數(shù),只會(huì)用常用的幾個(gè),例如 getchunk,為了這幾個(gè)函數(shù)全量引用還是有些浪費(fèi)的。

社區(qū)上面對(duì)這種場(chǎng)景,當(dāng)然也有優(yōu)化方案,比如說 lodash-es,以 ESM 的形式導(dǎo)出函數(shù),再借助 Webpack 等工具的 Tree Sharking 優(yōu)化,就可以只保留引用的文件。但是就如前面所說,React Native 的打包工具 Metro 不支持 Tree Shaking,所以對(duì)于 lodash-es 文件,其實(shí)還會(huì)全量引入,而且 lodash-es 的全量文件比 lodash 要大得多。

我做了個(gè)簡(jiǎn)單的測(cè)試,對(duì)于一個(gè)剛剛初始化的 React Native 應(yīng)用,全量引入 lodash 后,包體積增大了 71.23KB,全量引入 lodash-es 后,包體積會(huì)擴(kuò)大 173.85KB。

既然 lodash-es 不適合在 RN 中用,我們就只能在 lodash 上想辦法了。lodash 其實(shí)還有一種用法,那就是直接引用單文件,例如想用 join 這個(gè)方法,我們可以這樣引用:

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

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

這樣打包的時(shí)候就會(huì)只打包 lodash/join 這一個(gè)文件。

但是這樣做還是太麻煩了,比如說我們要使用 lodash 的七八個(gè)方法,那我們就要分別 import 七八次,非常的繁瑣。對(duì)于 lodash 這么熱門的工具庫,社區(qū)上肯定有高人安排好了,babel-plugin-lodash 這個(gè) babel 插件,可以在 JS 編譯時(shí)操作 AST 做如下的自動(dòng)轉(zhuǎn)換:

import { join, chunk } from 'lodash'
// ??
import join from 'lodash/join'
import chunk from 'lodash/chunk'

使用方式也很簡(jiǎn)單,首先運(yùn)行 yarn add babel-plugin-lodash -D 安裝,然后在 babel.config.js 文件里啟用插件即可:

// babel.config.js

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

我以 join 這個(gè)方法為例,大家可以看一下各個(gè)方法增加的 JS Bundle 體積:

全量 lodash 全量 loads-es lodash/join 單文件引用 lodash + babel-plugin-lodash
71.23 KB 173.85 KB 119 Bytes 119 Bytes

從表格可見 lodash 配合 babel-plugin-lodash 是最優(yōu)的開發(fā)選擇。

<br />

2.1.3 babel-plugin-import 的使用

babel-plugin-lodash 只能轉(zhuǎn)換 lodash 的引用問題,其實(shí)社區(qū)還有一個(gè)非常實(shí)用的 babel 插件:babel-plugin-import基本上它可以解決所有按需引用的問題。

我舉個(gè)簡(jiǎn)單的例子,阿里有個(gè)很好用的 ahooks 開源庫,封裝了很多常用的 React hooks,但問題是這個(gè)庫是針對(duì) Web 平臺(tái)封裝的,比如說 useTitle 這個(gè) hook,是用來設(shè)置網(wǎng)頁標(biāo)題的,但是 React Native 平臺(tái)是沒有相關(guān)的 BOM API 的,所以這個(gè) hooks 完全沒有必要引入,RN 也永遠(yuǎn)用不到這個(gè) API。

這時(shí)候我們就可以用 babel-plugin-import 實(shí)現(xiàn)按需引用了,假設(shè)我們只要用到 useInterval 這個(gè) Hooks,我們現(xiàn)在業(yè)務(wù)代碼中引入:

import { useInterval } from 'ahooks'

然后運(yùn)行 yarn add babel-plugin-import -D 安裝插件,在 babel.config.js 文件里啟用插件:

// babel.config.js

module.exports = {
  plugins: [
    [
      'import',
      {
        libraryName: 'ahooks',
        camel2DashComponentName: false, // 是否需要駝峰轉(zhuǎn)短線
        camel2UnderlineComponentName: false, // 是否需要駝峰轉(zhuǎn)下劃線
      },
    ],
  ],
  presets: ['module:metro-react-native-babel-preset'],
};

啟用后就可以實(shí)現(xiàn) ahooks 的按需引入:

import { useInterval } from 'ahooks'
// ??
import useInterval from 'ahooks/lib/useInterval'

下面是各種情況下的 JSBundle 體積增量,綜合來看 babel-plugin-import 是最優(yōu)的選擇:

全量 ahooks ahooks/lib/useInterval 單文件引用 ahooks + babel-plugin-import
111.41 KiB 443 Bytes 443 Bytes

當(dāng)然,babel-plugin-import 可以作用于很多的庫文件,比如說內(nèi)部/第三方封裝的 UI 組件,基本上都可以通過babel-plugin-import 的配置項(xiàng)實(shí)現(xiàn)按需引入。若有需求,可以看網(wǎng)上其他人總結(jié)的使用經(jīng)驗(yàn),我這里就不多言了。

<br />

2.1.4 babel-plugin-transform-remove-console

移除 console 的 babel 插件也很有用,我們可以配置它在打包發(fā)布的時(shí)候移除 console 語句,減小包體積的同時(shí)還會(huì)加快 JS 運(yùn)行速度,我們只要安裝后再簡(jiǎn)單的配置一下就好了:

// babel.config.js

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

<br />

2.1.5 制定良好的編碼規(guī)范

編碼規(guī)范的最佳實(shí)踐太多了,為了切合主題(減少代碼體積),我就隨便舉幾點(diǎn):

  • 代碼的抽象和復(fù)用:代碼中重復(fù)的邏輯根據(jù)可復(fù)用程度,盡量抽象為一個(gè)方法,不要用一次復(fù)制一次
  • 刪除無效的邏輯:這個(gè)也很常見,隨著業(yè)務(wù)的迭代,很多代碼都不會(huì)用了,如果某個(gè)功能下線了,就直接刪掉,哪天要用到再從 git 記錄里找
  • 刪除冗余的樣式:例如引入 ESLint plugin for React Native,開啟 "react-native/no-unused-styles" 選項(xiàng),借助 ESLint 提示無效的樣式文件

說實(shí)話這幾個(gè)優(yōu)化其實(shí)減少不了幾 KB 的代碼,更大的價(jià)值在于提升項(xiàng)目的健壯性和可維護(hù)性。

2.2 Inline Requires

Inline Requires 可以理解為懶執(zhí)行,注意我這里說的不是懶加載,因?yàn)橐话闱闆r下,RN 容器初始化之后會(huì)全量加載解析 JS Bundle 文件,Inline Requires 的作用是延遲運(yùn)行,也就是說只有需要使用的時(shí)候才會(huì)執(zhí)行 JS 代碼,而不是啟動(dòng)的時(shí)候就執(zhí)行。React Native 0.64 版本里,默認(rèn)開啟了 Inline Requires 。

首先我們要在 metro.config.js 里確認(rèn)開啟了 Inline Requires 功能:

// metro.config.js

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

其實(shí) Inline Requires 的原理非常簡(jiǎn)單,就是把 require 導(dǎo)入的位置改變了一下。

比如說我們寫了個(gè)工具函數(shù) join 放在 utils.js 文件里:

// utils.js

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

然后我們?cè)?App.js 里 import 這個(gè)庫:

// App.js

import { join } from 'my-module';

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

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

上面的寫法,被 Metro 編譯后,相當(dāng)于編譯成下面的樣子:

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

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

實(shí)際編譯后的代碼其實(shí)長(zhǎng)這個(gè)樣子:

rn_start_inlineRequire

上圖紅線中的 r() 函數(shù),其實(shí)是 RN 自己封裝的 require() 函數(shù),可以看出 Metro 自動(dòng)把頂層的 import 移動(dòng)到使用的位置。

值得注意的是,Metro 的自動(dòng) Inline Requires 配置,目前是不支持 export default 導(dǎo)出的,也就是說,如果你的 join 函數(shù)是這樣寫的:

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

導(dǎo)入時(shí)是這樣的:

import join from './utils';

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

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

Metro 編譯轉(zhuǎn)換后的代碼,對(duì)應(yīng)的 import 還是處于函數(shù)頂層

rn_start_require

這個(gè)需要特別注意一下,社區(qū)也有相關(guān)的文章,呼吁大家不要用 export default 這個(gè)語法,感興趣的可以了解一下:

深入解析 ES Module(一):禁用 export default object

深入解析 ES Module(二):徹底禁用 default export

2.3 JSBundle 分包加載

分包的場(chǎng)景一般出現(xiàn)在 Native 為主,React Native 為輔的場(chǎng)景里。這種場(chǎng)景往往是這樣的:

  • 假設(shè)有兩條基于 RN 的業(yè)務(wù)線 A 和 B,它們的 JSBundle 都是動(dòng)態(tài)下發(fā)的
  • A 的 JSBundle 大小為 700KB,其中包括 600KB 的基礎(chǔ)包(React,React Native 的 JS 代碼)和 100KB 的業(yè)務(wù)代碼
  • A 的 JSBundle 大小為 800KB,其中包括 600KB 的基礎(chǔ)包和 200KB 的業(yè)務(wù)代碼
  • 每次從 Native 跳轉(zhuǎn)到 A/B 的 RN 容器,都要全量下載解析運(yùn)行

大家從上面的例子里可以看出,600KB 的基礎(chǔ)包在多條業(yè)務(wù)線里是重復(fù)的,完全沒有必要多次下載和加載,這時(shí)候一個(gè)想法自然而然就出來了:

把一些共有庫打包到一個(gè) common.bundle 文件里,我們每次只要?jiǎng)討B(tài)下發(fā)業(yè)務(wù)包 businessA.bundlebusinessB.bundle,然后在客戶端實(shí)現(xiàn)先加載 common.bundle 文件,再加載 business.bundle 文件就可以了

這樣做的好處有幾個(gè):

  • common.bundle 可以直接放在本地,省去多業(yè)務(wù)線的多次下載,節(jié)省流量和帶寬
  • 可以在 RN 容器預(yù)初始化的時(shí)候就加載 common.bundle ,二次加載的業(yè)務(wù)包體積更小,初始化速度更快

順著上面的思路,上面問題就會(huì)轉(zhuǎn)換為兩個(gè)小問題:

  • 如何實(shí)現(xiàn) JSBundle 的拆包?
  • iOS/Android 的 RN 容器如何實(shí)現(xiàn)多 bundle 加載?

<br />

2.3.1 JS Bundle 拆包

拆包之前要先了解一下 Metro 這個(gè)打包工具的工作流程。Metro 的打包流程很簡(jiǎn)單,只有三個(gè)步驟:

  • Resolution:可以簡(jiǎn)單理解為分析各個(gè)模塊的依賴關(guān)系,最后會(huì)生成一個(gè)依賴圖
  • Transformation:代碼的編譯轉(zhuǎn)換,主要是借助 Babel 的編譯轉(zhuǎn)換能力
  • Serialization:所有代碼轉(zhuǎn)換完畢后,打印轉(zhuǎn)換后的代碼,生成一個(gè)或者多個(gè) bundle 文件

從上面流程可以看出,我們的拆包步驟只會(huì)在 Serialization 這一步。我們只要借助 Serialization 暴露的各個(gè)方法就可以實(shí)現(xiàn) bundle 分包了。

正式分包前,我們先拋開各種技術(shù)細(xì)節(jié),把問題簡(jiǎn)化一下:對(duì)于一個(gè)全是數(shù)字的數(shù)組,如何把它分為偶數(shù)數(shù)組和奇數(shù)數(shù)組?

這個(gè)問題太簡(jiǎn)單了,剛學(xué)編程的人應(yīng)該都能想到答案,遍歷一遍原數(shù)組,如果當(dāng)前元素是奇數(shù),就放到奇數(shù)數(shù)組里,如果是偶數(shù),放偶數(shù)數(shù)組里。

Metro 對(duì) JS bundle 分包其實(shí)是一個(gè)道理。Metro 打包的時(shí)候,會(huì)給每個(gè)模塊設(shè)置 moduleId,這個(gè) id 就是一個(gè)從 0 開始的自增 number。我們分包的時(shí)候,公有的模塊(例如 react react-native)輸出到 common.bundle,業(yè)務(wù)模塊輸出到 business.bundle 就行了。

因?yàn)橐骖櫠鄺l業(yè)務(wù)線,現(xiàn)在業(yè)內(nèi)主流的分包方案是這樣的:

1.先建立一個(gè) common.js 文件,里面引入了所有的公有模塊,然后 Metro 以這個(gè) common.js 為入口文件,打一個(gè) common.bundle 文件,同時(shí)要記錄所有的公有模塊的 moduleId

// common.js

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

2. 對(duì)業(yè)務(wù)線 A 進(jìn)行打包,Metro 的打包入口文件就是 A 的項(xiàng)目入口文件。打包過程中要過濾掉上一步記錄的公有模塊 moduleId,這樣打包結(jié)果就只有 A 的業(yè)務(wù)代碼了

// indexA.js

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

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

3. 業(yè)務(wù)線 B C D E...... 打包流程同業(yè)務(wù)線 A

<br />

上面的思路看起來很美好,但是還是存在一個(gè)問題:每次啟動(dòng) Metro 打包的時(shí)候,moduleId 都是從 0 開始自增,這樣會(huì)導(dǎo)致不同的 JSBundle ID 重復(fù)。

為了避免 id 重復(fù),目前業(yè)內(nèi)主流的做法是把模塊的路徑當(dāng)作 moduleId(因?yàn)槟K的路徑基本上是固定且不沖突的),這樣就解決了 id 沖突的問題。Metro 暴露了 createModuleIdFactory 這個(gè)函數(shù),我們可以在這個(gè)函數(shù)里覆蓋原來的自增 number 邏輯:

module.exports = {
  serializer: {
    createModuleIdFactory: function () {
      return function (path) {
        // 根據(jù)文件的相對(duì)路徑構(gòu)建 ModuleId
        const projectRootPath = __dirname;
        let moduleId = path.substr(projectRootPath.length + 1);
        return moduleId;
      };
    },
  },
};

<br />

整合一下第一步的思路,就可以構(gòu)建出下面的 metro.common.config.js 配置文件:

// metro.common.config.js

const fs = require('fs');

module.exports = {
  transformer: {
    getTransformOptions: async () => ({
      transform: {
        experimentalImportSupport: false,
        inlineRequires: true,
      },
    }),
  },
  serializer: {
    createModuleIdFactory: function () {
      return function (path) {
        // 根據(jù)文件的相對(duì)路徑構(gòu)建 ModuleId
        const projectRootPath = __dirname;
        let moduleId = path.substr(projectRootPath.length + 1);
        
        // 把 moduleId 寫入 idList.txt 文件,記錄公有模塊 id
        fs.appendFileSync('./idList.txt', `${moduleId}\n`);
        return moduleId;
      };
    },
  },
};

然后運(yùn)行命令行命令打包即可:

# 打包平臺(tái):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

通過以上命令的打包,我們可以看到 moduleId 都轉(zhuǎn)換為了相對(duì)路徑,并且 idList.txt 也記錄了所有的 moduleId:

common.android.bundle
idList.js

<br />

第二步的關(guān)鍵在于過濾公有模塊的 moduleId,Metro 提供了 processModuleFilter 這個(gè)方法,借助它可以實(shí)現(xiàn)模塊的過濾。具體的邏輯可見以下代碼:

// metro.business.config.js

const fs = require('fs');

// 讀取 idList.txt,轉(zhuǎn)換為數(shù)組
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 里的數(shù)據(jù)
      if (idList.indexOf(mouduleId) < 0) {
        console.log('createModuleIdFactory path', mouduleId);
        return true;
      }
      return false;
    },
  },
};

最后運(yùn)行命令行命令打包即可:

# 打包平臺(tái):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

最后的打包結(jié)果只有 11 行(不分包的話得 398 行),可以看出分包的收益非常大。

business.android.bundle

當(dāng)然使用相對(duì)路徑作為 moduleId 打包時(shí),不可避免的會(huì)導(dǎo)致包體積變大,我們可以使用 md5 計(jì)算一下相對(duì)路徑,然后取前幾位作為最后的 moduleId;或者還是采用遞增 id,只不過使用更復(fù)雜的映射算法來保證 moduleId 的唯一性和穩(wěn)定性。這部分的內(nèi)容其實(shí)屬于非常經(jīng)典的 Map key 設(shè)計(jì)問題,感興趣的讀者可以了解學(xué)習(xí)一下相關(guān)的算法理論知識(shí)。

<br />

2.3.2 Native 實(shí)現(xiàn)多 bundle 加載

分包只是第一步,想要展示完整正確的 RN 界面,還需要做到「合」,這個(gè)「合」就是指在 Native 端實(shí)現(xiàn)多 bundle 的加載。

common.bundle 的加載比較容易,直接在 RN 容器初始化的時(shí)候加載就好了。容器初始化的流程上一節(jié)我已經(jīng)詳細(xì)介紹了,這里就不多言了。這時(shí)候問題就轉(zhuǎn)換為 business.bundle 的加載問題。

React Native 不像瀏覽器的多 bundle 加載,直接動(dòng)態(tài)生成一個(gè) <script /> 標(biāo)簽插入 HTML 中就可以實(shí)現(xiàn)動(dòng)態(tài)加載了。我們需要結(jié)合具體的 RN 容器實(shí)現(xiàn)來實(shí)現(xiàn) business.bundle 加載的需求。這時(shí)候我們需要關(guān)注兩個(gè)點(diǎn):

  1. 時(shí)機(jī):什么時(shí)候開始加載?
  2. 方法:如何加載新的 bundle?

<br />

對(duì)于第一個(gè)問題,我們的答案是 common.bundle 加載完成后再加載 business.bundle

common.bundle 加載完成后,iOS 端會(huì)發(fā)送事件名稱是 RCTJavaScriptDidLoadNotification 的全局通知,Android 端則會(huì)向 ReactInstanceManager 實(shí)例中注冊(cè)的所有 ReactInstanceEventListener 回調(diào) onReactContextInitialized() 方法。我們?cè)趯?duì)應(yīng)事件監(jiān)聽器和回調(diào)中實(shí)現(xiàn)業(yè)務(wù)包的加載即可。

<br />

對(duì)于第二個(gè)問題,iOS 我們可以使用 RCTCxxBridge 的 executeSourceCode 方法在當(dāng)前的 RN 實(shí)例上下文中執(zhí)行一段 JS 代碼,以此來達(dá)到增量加載的目的。不過值得注意的是,executeSourceCode 是 RCTCxxBridge 的私有方法,需要我們用 Category 將其暴露出來。

Android 端可以使用剛剛建立好的 ReactInstanceManager 實(shí)例,通過 getCurrentReactContext() 獲取到當(dāng)前的 ReactContext 上下文對(duì)象,再調(diào)用上下文對(duì)象的 getCatalystInstance() 方法獲取媒介實(shí)例,最終調(diào)用媒介實(shí)例的 loadScriptFromFile(String fileName, String sourceURL, boolean loadSynchronously) 方法完成業(yè)務(wù) JSBundle 的增量加載。

iOS 和 Android 的示例代碼如下:

NSURL *businessBundleURI = // 業(yè)務(wù)包 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);

<br />


本小節(jié)的示例代碼都屬于 demo 級(jí)別,如果想要真正接入生產(chǎn)環(huán)境,需要結(jié)合實(shí)際的架構(gòu)和業(yè)務(wù)場(chǎng)景做定制。有一個(gè) React Native 分包倉庫 react-native-multibundler 內(nèi)容挺不錯(cuò)的,大家可以參考學(xué)習(xí)一下。

3.Network

rn_start_network

我們一般會(huì)在 React Component 的 componentDidMount() 執(zhí)行后請(qǐng)求網(wǎng)絡(luò),從服務(wù)器獲取數(shù)據(jù),然后再改變 Component 的 state 進(jìn)行數(shù)據(jù)的渲染。

網(wǎng)絡(luò)優(yōu)化是一個(gè)非常龐大非常獨(dú)立的話題,有非常多的點(diǎn)可以優(yōu)化,我這里列舉幾個(gè)和首屏加載相關(guān)的網(wǎng)絡(luò)優(yōu)化點(diǎn):

  • DNS 緩存:提前緩存 IP 地址,跳過 DNS 尋址時(shí)間
  • 緩存復(fù)用:進(jìn)入 RN 頁面前,先提前請(qǐng)求網(wǎng)絡(luò)數(shù)據(jù)并緩存下來,打開 RN 頁面后請(qǐng)求網(wǎng)絡(luò)前先檢查緩存數(shù)據(jù),如果緩存未過期,直接從本地緩存里拿數(shù)據(jù)
  • 請(qǐng)求合并:如果還在用 HTTP/1.1,若首屏有多個(gè)請(qǐng)求,可以合并多個(gè)請(qǐng)求為一個(gè)請(qǐng)求
  • HTTP2:利用 HTTP2 的并行請(qǐng)求和多路復(fù)用優(yōu)化速度
  • 減小體積:去除接口的冗余字段,減少圖片資源的體積等等
  • ......

由于網(wǎng)絡(luò)這里相對(duì)來說比較獨(dú)立,iOS/Android/Web 的優(yōu)化經(jīng)驗(yàn)其實(shí)都可以用到 RN 上,這里按照大家以往的優(yōu)化經(jīng)驗(yàn)來就可以了。

4.Render

rn_start_render

渲染這里的耗時(shí),基本上和首屏頁面的 UI 復(fù)雜度成正相關(guān)??梢酝ㄟ^渲染流程查看哪里會(huì)出現(xiàn)耗時(shí):

  • VDOM 計(jì)算:頁面復(fù)雜度越高,JavaScript 側(cè)的計(jì)算耗時(shí)就會(huì)越長(zhǎng)(VDOM 的生成與 Diff)
  • JS Native 通訊:JS 的計(jì)算結(jié)果會(huì)轉(zhuǎn)為 JSON 通過 Bridge 傳遞給 Native 側(cè),復(fù)雜度越高,JSON 的數(shù)據(jù)量越大,有可能阻塞 Bridge 通訊
  • Native 渲染:Native 側(cè)遞歸解析 render tree,布局越復(fù)雜,渲染時(shí)間越長(zhǎng)

我們可以在代碼里開啟 MessageQueue 監(jiān)視,看看 APP 啟動(dòng)后 JS Bridge 上面有有些啥:

// index.js

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

從圖片里可以看出 JS 加載完畢后有大量和 UI 相關(guān)的 UIManager.createView() UIManager.setChildren() 通訊,結(jié)合上面的耗時(shí)總結(jié),我們對(duì)應(yīng)著就有幾條解決方案:

  • 通過一定的布局技巧降低 UI 嵌套層級(jí),降低 UI 視圖的復(fù)雜度

  • 減少 re-render,直接在 JS 側(cè)截?cái)嘀乩L流程,減少 bridge 通訊的頻率和數(shù)據(jù)量

  • 如果是 React Native 為主架構(gòu)的 APP,首屏可以直接替換為 Native View,直接脫離 RN 的渲染流程

上面的這些技巧我都在舊文《React Native 性能優(yōu)化指南——渲染篇》里做了詳細(xì)的解釋,這里就不多解釋了。

Fraic

從上面的我們可以看出,React Native 的渲染需要在 Bridge 上傳遞大量的 JSON 數(shù)據(jù),在 React Native 初始化時(shí),數(shù)據(jù)量過大會(huì)阻塞 bridge,拖慢我們的啟動(dòng)和渲染速度。React Native 新架構(gòu)中的 Fraic 就能解決這一問題,JS 和 Native UI 不再是異步的通訊,可以實(shí)現(xiàn)直接的調(diào)用,可以大大加速渲染性能。

Fraic 可以說是 RN 新架構(gòu)里最讓人期待的了,想了解更多內(nèi)容,可以去官方 issues 區(qū)圍觀。

總結(jié)

本文主要從 JavaScript 的角度出發(fā),分析了 Hermes 引擎的特點(diǎn)和作用,并總結(jié)分析了 JSBundle 的各種優(yōu)化手段,再結(jié)合網(wǎng)絡(luò)和渲染優(yōu)化,全方位提升 React Native 應(yīng)用的啟動(dòng)速度。

參考

?? React Native 啟動(dòng)速度優(yōu)化——Native 篇(內(nèi)含源碼分析)

?? React Native 性能優(yōu)化指南——渲染篇

?? 移動(dòng)端 JS 引擎哪家強(qiáng)?

招商證券 react-native 熱更新優(yōu)化實(shí)踐

React Native中如何實(shí)現(xiàn)拆包?


如果你喜歡我的文章,希望點(diǎn)贊?? 收藏 ?? 評(píng)論 ?? 三連支持一下,謝謝你,這對(duì)我真的很重要!

歡迎大家關(guān)注我的微信公眾號(hào):鹵蛋實(shí)驗(yàn)室,目前專注前端技術(shù),對(duì)圖形學(xué)也有一些微小研究。

原文鏈接 ?? ?? React Native 啟動(dòng)速度優(yōu)化——JS 篇:更新更及時(shí),閱讀體驗(yàn)更佳

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容