如果你喜歡我的文章,希望點贊?? 收藏 ?? 評論 ?? 三連支持一下,謝謝你,這對我真的很重要!
前言
上一篇文章主要從 Native 的角度分析了 React Native 的初始化流程,并從源碼出發(fā),總結(jié)了幾個 React Native 容器初始化的優(yōu)化點。本文主要從 JavaScript 入手,總結(jié)了一些 JS 側(cè)的優(yōu)化要點。
1.JSEngine
Hermes
Hermes 是 FaceBook 2019 年中旬開源的一款 JS 引擎,從 release 記錄可以看出,這個是專為 React Native 打造的 JS 引擎,可以說從設(shè)計之初就是為 Hybrid UI 系統(tǒng)打造。
Hermes 支持直接加載字節(jié)碼,也就是說,Babel
、Minify
、Parse
和 Compile
這些流程全部都在開發(fā)者電腦上完成,直接下發(fā)字節(jié)碼讓 Hermes 運行就行,這樣做可以省去 JSEngine 解析編譯 JavaScript 的流程,JS 代碼的加載速度將會大大加快,啟動速度也會有非常大的提升。
更多關(guān)于 Hermes 的特性,大家可以看我的舊文《移動端 JS 引擎哪家強》這篇文章,我做了更為詳細的特性說明與數(shù)據(jù)對比,這里就不多說了。
2.JS Bundle
前面的優(yōu)化其實都是 Native 層的優(yōu)化,從這里開始就進入 Web 前端最熟悉的領(lǐng)域了。
其實談到 JS Bundle 的優(yōu)化,來來回回就是那么幾條路:
- 縮:縮小 Bundle 的總體積,減少 JS 加載和解析的時間
- 延:動態(tài)導(dǎo)入(dynamic import),懶加載,按需加載,延遲執(zhí)行
- 拆:拆分公共模塊和業(yè)務(wù)模塊,避免公共模塊重復(fù)引入
如果有 webpack 打包優(yōu)化經(jīng)驗的小伙伴,看到上面的優(yōu)化方式,是不是腦海中已經(jīng)浮現(xiàn)出 webpack 的一些配置項了?不過 React Native 的打包工具不是 webpack 而是 Facebook 自研的 Metro,雖然配置細節(jié)不一樣,但道理是相通的,下面我就這幾個點講講 React Native 如何優(yōu)化 JS Bundle。
2.1 減小 JS Bundle 體積
Metro 打包 JS 時,會把 ESM 模塊轉(zhuǎn)為 CommonJS 模塊,這就導(dǎo)致現(xiàn)在比較火的依賴于 ESM 的 Tree Shaking 完全不起作用,而且根據(jù)官方回復(fù),Metro 未來也不會支持 Tree Shaking :
因為這個原因,我們減小 bundle 體積主要是三個方向:
- 對于同樣的功能,優(yōu)先選擇體積更小的第三方庫
- 利用 babel 插件,避免全量引用
- 制定編碼規(guī)范,減少重復(fù)代碼
下面我們舉幾個例子來解釋上面的三個思路。
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)系:
使用非常簡單,按照文檔安裝分析就可。
<br />
2.1.1 moment.js 替換為 day.js
這是一個非常經(jīng)典的例子。同樣是時間格式化的第三方庫, moment.js 體積 200 KB,day.js 體積只有 2KB,而且 API 與 moment.js 保持一致。如果項目里用了 moment.js,替換為 day.js 后可以立馬減少 JSBundle 的體積。
<br />
2.1.2 lodah.js 配合 babel-plugin-lodash
lodash 基本上屬于 Web 前端的工程標配了,但是對于大多數(shù)人來說,對于 lodash 封裝的近 300 個函數(shù),只會用常用的幾個,例如 get
、 chunk
,為了這幾個函數(shù)全量引用還是有些浪費的。
社區(qū)上面對這種場景,當(dāng)然也有優(yōu)化方案,比如說 lodash-es
,以 ESM 的形式導(dǎo)出函數(shù),再借助 Webpack 等工具的 Tree Sharking 優(yōu)化,就可以只保留引用的文件。但是就如前面所說,React Native 的打包工具 Metro 不支持 Tree Shaking,所以對于 lodash-es
文件,其實還會全量引入,而且 lodash-es
的全量文件比 lodash
要大得多。
我做了個簡單的測試,對于一個剛剛初始化的 React Native 應(yīng)用,全量引入 lodash 后,包體積增大了 71.23KB,全量引入 lodash-es
后,包體積會擴大 173.85KB。
既然 lodash-es
不適合在 RN 中用,我們就只能在 lodash
上想辦法了。lodash 其實還有一種用法,那就是直接引用單文件,例如想用 join
這個方法,我們可以這樣引用:
// 全量
import { join } from 'lodash'
// 單文件引用
import join from 'lodash/join'
這樣打包的時候就會只打包 lodash/join
這一個文件。
但是這樣做還是太麻煩了,比如說我們要使用 lodash 的七八個方法,那我們就要分別 import 七八次,非常的繁瑣。對于 lodash 這么熱門的工具庫,社區(qū)上肯定有高人安排好了,babel-plugin-lodash
這個 babel 插件,可以在 JS 編譯時操作 AST 做如下的自動轉(zhuǎn)換:
import { join, chunk } from 'lodash'
// ??
import join from 'lodash/join'
import chunk from 'lodash/chunk'
使用方式也很簡單,首先運行 yarn add babel-plugin-lodash -D
安裝,然后在 babel.config.js
文件里啟用插件即可:
// babel.config.js
module.exports = {
plugins: ['lodash'],
presets: ['module:metro-react-native-babel-preset'],
};
我以 join 這個方法為例,大家可以看一下各個方法增加的 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
的引用問題,其實社區(qū)還有一個非常實用的 babel 插件:babel-plugin-import
,基本上它可以解決所有按需引用的問題。
我舉個簡單的例子,阿里有個很好用的 ahooks 開源庫,封裝了很多常用的 React hooks,但問題是這個庫是針對 Web 平臺封裝的,比如說 useTitle
這個 hook,是用來設(shè)置網(wǎng)頁標題的,但是 React Native 平臺是沒有相關(guān)的 BOM API 的,所以這個 hooks 完全沒有必要引入,RN 也永遠用不到這個 API。
這時候我們就可以用 babel-plugin-import
實現(xiàn)按需引用了,假設(shè)我們只要用到 useInterval
這個 Hooks,我們現(xiàn)在業(yè)務(wù)代碼中引入:
import { useInterval } from 'ahooks'
然后運行 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'],
};
啟用后就可以實現(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àn)按需引入。若有需求,可以看網(wǎng)上其他人總結(jié)的使用經(jīng)驗,我這里就不多言了。
<br />
2.1.4 babel-plugin-transform-remove-console
移除 console 的 babel 插件也很有用,我們可以配置它在打包發(fā)布的時候移除 console
語句,減小包體積的同時還會加快 JS 運行速度,我們只要安裝后再簡單的配置一下就好了:
// babel.config.js
module.exports = {
presets: ['module:metro-react-native-babel-preset'],
env: {
production: {
plugins: ['transform-remove-console'],
},
},
};
<br />
2.1.5 制定良好的編碼規(guī)范
編碼規(guī)范的最佳實踐太多了,為了切合主題(減少代碼體積),我就隨便舉幾點:
- 代碼的抽象和復(fù)用:代碼中重復(fù)的邏輯根據(jù)可復(fù)用程度,盡量抽象為一個方法,不要用一次復(fù)制一次
- 刪除無效的邏輯:這個也很常見,隨著業(yè)務(wù)的迭代,很多代碼都不會用了,如果某個功能下線了,就直接刪掉,哪天要用到再從 git 記錄里找
-
刪除冗余的樣式:例如引入 ESLint plugin for React Native,開啟
"react-native/no-unused-styles"
選項,借助 ESLint 提示無效的樣式文件
說實話這幾個優(yōu)化其實減少不了幾 KB 的代碼,更大的價值在于提升項目的健壯性和可維護性。
2.2 Inline Requires
Inline Requires
可以理解為懶執(zhí)行,注意我這里說的不是懶加載,因為一般情況下,RN 容器初始化之后會全量加載解析 JS Bundle 文件,Inline Requires
的作用是延遲運行,也就是說只有需要使用的時候才會執(zhí)行 JS 代碼,而不是啟動的時候就執(zhí)行。React Native 0.64 版本里,默認開啟了 Inline Requires
。
首先我們要在 metro.config.js
里確認開啟了 Inline Requires
功能:
// metro.config.js
module.exports = {
transformer: {
getTransformOptions: async () => ({
transform: {
experimentalImportSupport: false,
inlineRequires: true, // <-- here
},
}),
},
};
其實 Inline Requires
的原理非常簡單,就是把 require 導(dǎo)入的位置改變了一下。
比如說我們寫了個工具函數(shù) join
放在 utils.js
文件里:
// utils.js
export function join(list, j) {
return list.join(j);
}
然后我們在 App.js
里 import 這個庫:
// 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>;
};
實際編譯后的代碼其實長這個樣子:
上圖紅線中的 r()
函數(shù),其實是 RN 自己封裝的 require()
函數(shù),可以看出 Metro 自動把頂層的 import 移動到使用的位置。
值得注意的是,Metro 的自動 Inline Requires
配置,目前是不支持 export default
導(dǎo)出的,也就是說,如果你的 join 函數(shù)是這樣寫的:
export default function join(list, j) {
return list.join(j);
}
導(dǎo)入時是這樣的:
import join from './utils';
const App = (props) => {
const result = join(['a', 'b', 'c'], '~');
return <Text>{result}</Text>;
};
Metro 編譯轉(zhuǎn)換后的代碼,對應(yīng)的 import 還是處于函數(shù)頂層:
這個需要特別注意一下,社區(qū)也有相關(guān)的文章,呼吁大家不要用 export default
這個語法,感興趣的可以了解一下:
深入解析 ES Module(一):禁用 export default object
深入解析 ES Module(二):徹底禁用 default export
2.3 JSBundle 分包加載
分包的場景一般出現(xiàn)在 Native 為主,React Native 為輔的場景里。這種場景往往是這樣的:
- 假設(shè)有兩條基于 RN 的業(yè)務(wù)線 A 和 B,它們的 JSBundle 都是動態(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 容器,都要全量下載解析運行
大家從上面的例子里可以看出,600KB 的基礎(chǔ)包在多條業(yè)務(wù)線里是重復(fù)的,完全沒有必要多次下載和加載,這時候一個想法自然而然就出來了:
把一些共有庫打包到一個
common.bundle
文件里,我們每次只要動態(tài)下發(fā)業(yè)務(wù)包businessA.bundle
和businessB.bundle
,然后在客戶端實現(xiàn)先加載common.bundle
文件,再加載business.bundle
文件就可以了
這樣做的好處有幾個:
-
common.bundle
可以直接放在本地,省去多業(yè)務(wù)線的多次下載,節(jié)省流量和帶寬 - 可以在 RN 容器預(yù)初始化的時候就加載
common.bundle
,二次加載的業(yè)務(wù)包體積更小,初始化速度更快
順著上面的思路,上面問題就會轉(zhuǎn)換為兩個小問題:
- 如何實現(xiàn) JSBundle 的拆包?
- iOS/Android 的 RN 容器如何實現(xiàn)多 bundle 加載?
<br />
2.3.1 JS Bundle 拆包
拆包之前要先了解一下 Metro 這個打包工具的工作流程。Metro 的打包流程很簡單,只有三個步驟:
- Resolution:可以簡單理解為分析各個模塊的依賴關(guān)系,最后會生成一個依賴圖
- Transformation:代碼的編譯轉(zhuǎn)換,主要是借助 Babel 的編譯轉(zhuǎn)換能力
- Serialization:所有代碼轉(zhuǎn)換完畢后,打印轉(zhuǎn)換后的代碼,生成一個或者多個 bundle 文件
從上面流程可以看出,我們的拆包步驟只會在 Serialization
這一步。我們只要借助 Serialization
暴露的各個方法就可以實現(xiàn) bundle 分包了。
正式分包前,我們先拋開各種技術(shù)細節(jié),把問題簡化一下:對于一個全是數(shù)字的數(shù)組,如何把它分為偶數(shù)數(shù)組和奇數(shù)數(shù)組?
這個問題太簡單了,剛學(xué)編程的人應(yīng)該都能想到答案,遍歷一遍原數(shù)組,如果當(dāng)前元素是奇數(shù),就放到奇數(shù)數(shù)組里,如果是偶數(shù),放偶數(shù)數(shù)組里。
Metro 對 JS bundle 分包其實是一個道理。Metro 打包的時候,會給每個模塊設(shè)置 moduleId,這個 id 就是一個從 0 開始的自增 number。我們分包的時候,公有的模塊(例如 react
react-native
)輸出到 common.bundle
,業(yè)務(wù)模塊輸出到 business.bundle
就行了。
因為要兼顧多條業(yè)務(wù)線,現(xiàn)在業(yè)內(nèi)主流的分包方案是這樣的:
1.先建立一個 common.js
文件,里面引入了所有的公有模塊,然后 Metro 以這個 common.js
為入口文件,打一個 common.bundle
文件,同時要記錄所有的公有模塊的 moduleId
// common.js
require('react');
require('react-native');
......
2. 對業(yè)務(wù)線 A 進行打包,Metro 的打包入口文件就是 A 的項目入口文件。打包過程中要過濾掉上一步記錄的公有模塊 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 />
上面的思路看起來很美好,但是還是存在一個問題:每次啟動 Metro 打包的時候,moduleId 都是從 0 開始自增,這樣會導(dǎo)致不同的 JSBundle ID 重復(fù)。
為了避免 id 重復(fù),目前業(yè)內(nèi)主流的做法是把模塊的路徑當(dāng)作 moduleId(因為模塊的路徑基本上是固定且不沖突的),這樣就解決了 id 沖突的問題。Metro 暴露了 createModuleIdFactory
這個函數(shù),我們可以在這個函數(shù)里覆蓋原來的自增 number 邏輯:
module.exports = {
serializer: {
createModuleIdFactory: function () {
return function (path) {
// 根據(jù)文件的相對路徑構(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ù)文件的相對路徑構(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;
};
},
},
};
然后運行命令行命令打包即可:
# 打包平臺: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)換為了相對路徑,并且 idList.txt
也記錄了所有的 moduleId:
<br />
第二步的關(guān)鍵在于過濾公有模塊的 moduleId,Metro 提供了 processModuleFilter
這個方法,借助它可以實現(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;
},
},
};
最后運行命令行命令打包即可:
# 打包平臺: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 行),可以看出分包的收益非常大。
當(dāng)然使用相對路徑作為 moduleId 打包時,不可避免的會導(dǎo)致包體積變大,我們可以使用 md5 計算一下相對路徑,然后取前幾位作為最后的 moduleId;或者還是采用遞增 id,只不過使用更復(fù)雜的映射算法來保證 moduleId 的唯一性和穩(wěn)定性。這部分的內(nèi)容其實屬于非常經(jīng)典的 Map key 設(shè)計問題,感興趣的讀者可以了解學(xué)習(xí)一下相關(guān)的算法理論知識。
<br />
2.3.2 Native 實現(xiàn)多 bundle 加載
分包只是第一步,想要展示完整正確的 RN 界面,還需要做到「合」,這個「合」就是指在 Native 端實現(xiàn)多 bundle 的加載。
common.bundle 的加載比較容易,直接在 RN 容器初始化的時候加載就好了。容器初始化的流程上一節(jié)我已經(jīng)詳細介紹了,這里就不多言了。這時候問題就轉(zhuǎn)換為 business.bundle
的加載問題。
React Native 不像瀏覽器的多 bundle 加載,直接動態(tài)生成一個 <script />
標簽插入 HTML 中就可以實現(xiàn)動態(tài)加載了。我們需要結(jié)合具體的 RN 容器實現(xiàn)來實現(xiàn) business.bundle
加載的需求。這時候我們需要關(guān)注兩個點:
- 時機:什么時候開始加載?
- 方法:如何加載新的 bundle?
<br />
對于第一個問題,我們的答案是 common.bundle
加載完成后再加載 business.bundle
。
common.bundle
加載完成后,iOS 端會發(fā)送事件名稱是 RCTJavaScriptDidLoadNotification
的全局通知,Android 端則會向 ReactInstanceManager 實例中注冊的所有 ReactInstanceEventListener 回調(diào) onReactContextInitialized()
方法。我們在對應(yīng)事件監(jiān)聽器和回調(diào)中實現(xiàn)業(yè)務(wù)包的加載即可。
<br />
對于第二個問題,iOS 我們可以使用 RCTCxxBridge 的 executeSourceCode
方法在當(dāng)前的 RN 實例上下文中執(zhí)行一段 JS 代碼,以此來達到增量加載的目的。不過值得注意的是,executeSourceCode
是 RCTCxxBridge 的私有方法,需要我們用 Category 將其暴露出來。
Android 端可以使用剛剛建立好的 ReactInstanceManager 實例,通過 getCurrentReactContext()
獲取到當(dāng)前的 ReactContext 上下文對象,再調(diào)用上下文對象的 getCatalystInstance()
方法獲取媒介實例,最終調(diào)用媒介實例的 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 級別,如果想要真正接入生產(chǎn)環(huán)境,需要結(jié)合實際的架構(gòu)和業(yè)務(wù)場景做定制。有一個 React Native 分包倉庫 react-native-multibundler 內(nèi)容挺不錯的,大家可以參考學(xué)習(xí)一下。
3.Network
我們一般會在 React Component 的 componentDidMount()
執(zhí)行后請求網(wǎng)絡(luò),從服務(wù)器獲取數(shù)據(jù),然后再改變 Component 的 state 進行數(shù)據(jù)的渲染。
網(wǎng)絡(luò)優(yōu)化是一個非常龐大非常獨立的話題,有非常多的點可以優(yōu)化,我這里列舉幾個和首屏加載相關(guān)的網(wǎng)絡(luò)優(yōu)化點:
- DNS 緩存:提前緩存 IP 地址,跳過 DNS 尋址時間
- 緩存復(fù)用:進入 RN 頁面前,先提前請求網(wǎng)絡(luò)數(shù)據(jù)并緩存下來,打開 RN 頁面后請求網(wǎng)絡(luò)前先檢查緩存數(shù)據(jù),如果緩存未過期,直接從本地緩存里拿數(shù)據(jù)
- 請求合并:如果還在用 HTTP/1.1,若首屏有多個請求,可以合并多個請求為一個請求
- HTTP2:利用 HTTP2 的并行請求和多路復(fù)用優(yōu)化速度
- 減小體積:去除接口的冗余字段,減少圖片資源的體積等等
- ......
由于網(wǎng)絡(luò)這里相對來說比較獨立,iOS/Android/Web 的優(yōu)化經(jīng)驗其實都可以用到 RN 上,這里按照大家以往的優(yōu)化經(jīng)驗來就可以了。
4.Render
渲染這里的耗時,基本上和首屏頁面的 UI 復(fù)雜度成正相關(guān)。可以通過渲染流程查看哪里會出現(xiàn)耗時:
- VDOM 計算:頁面復(fù)雜度越高,JavaScript 側(cè)的計算耗時就會越長(VDOM 的生成與 Diff)
- JS Native 通訊:JS 的計算結(jié)果會轉(zhuǎn)為 JSON 通過 Bridge 傳遞給 Native 側(cè),復(fù)雜度越高,JSON 的數(shù)據(jù)量越大,有可能阻塞 Bridge 通訊
- Native 渲染:Native 側(cè)遞歸解析 render tree,布局越復(fù)雜,渲染時間越長
我們可以在代碼里開啟 MessageQueue
監(jiān)視,看看 APP 啟動后 JS Bridge 上面有有些啥:
// index.js
import MessageQueue from 'react-native/Libraries/BatchedBridge/MessageQueue'
MessageQueue.spy(true);
從圖片里可以看出 JS 加載完畢后有大量和 UI 相關(guān)的 UIManager.createView()
UIManager.setChildren()
通訊,結(jié)合上面的耗時總結(jié),我們對應(yīng)著就有幾條解決方案:
通過一定的布局技巧降低 UI 嵌套層級,降低 UI 視圖的復(fù)雜度
減少 re-render,直接在 JS 側(cè)截斷重繪流程,減少 bridge 通訊的頻率和數(shù)據(jù)量
如果是 React Native 為主架構(gòu)的 APP,首屏可以直接替換為 Native View,直接脫離 RN 的渲染流程
上面的這些技巧我都在舊文《React Native 性能優(yōu)化指南——渲染篇》里做了詳細的解釋,這里就不多解釋了。
Fraic
從上面的我們可以看出,React Native 的渲染需要在 Bridge 上傳遞大量的 JSON 數(shù)據(jù),在 React Native 初始化時,數(shù)據(jù)量過大會阻塞 bridge,拖慢我們的啟動和渲染速度。React Native 新架構(gòu)中的 Fraic 就能解決這一問題,JS 和 Native UI 不再是異步的通訊,可以實現(xiàn)直接的調(diào)用,可以大大加速渲染性能。
Fraic 可以說是 RN 新架構(gòu)里最讓人期待的了,想了解更多內(nèi)容,可以去官方 issues 區(qū)圍觀。
總結(jié)
本文主要從 JavaScript 的角度出發(fā),分析了 Hermes 引擎的特點和作用,并總結(jié)分析了 JSBundle 的各種優(yōu)化手段,再結(jié)合網(wǎng)絡(luò)和渲染優(yōu)化,全方位提升 React Native 應(yīng)用的啟動速度。
參考
?? React Native 啟動速度優(yōu)化——Native 篇(內(nèi)含源碼分析)
?? React Native 性能優(yōu)化指南——渲染篇
招商證券 react-native 熱更新優(yōu)化實踐
如果你喜歡我的文章,希望點贊?? 收藏 ?? 評論 ?? 三連支持一下,謝謝你,這對我真的很重要!
歡迎大家關(guān)注我的微信公眾號:鹵蛋實驗室,目前專注前端技術(shù),對圖形學(xué)也有一些微小研究。
原文鏈接 ?? ?? React Native 啟動速度優(yōu)化——JS 篇:更新更及時,閱讀體驗更佳