摘要
對React-Native包進行劃分是優(yōu)化App啟動和內存占用的關鍵處理步驟飞袋,為此提出了一種基于splitChunk的分包方式嚎卫。對原始React-Native項目的多入口entryPoint進行分包,而這些多入口entryPoint之間的共同依賴通過設置splitChunk配置來提取到新的bundle中倒信,在加載一個entryPoint對應的bundle時钳踊,首先遞歸加載該bundle依賴的其他bundle,然后再加載entryPoint自身Bundle大莫。使用splitChunk進行分包管理蛉腌,可以便捷地管理多個Bundle之間的依賴引用關系,保證在加載Bundle時僅僅加載當前Bundle所依賴的模塊只厘,避免加載多余模塊烙丛。實驗結果表明,本方法能夠對React-Native包進行合理劃分羔味,并最小化App啟動時基礎包的體積河咽,提高App啟動速度,并減少App啟動時的內存占用赋元。
相關工作
React-Native在分包時主要工作集中在依賴管理忘蟹,目前項目的分包方案只拆分出了一部分業(yè)務模塊飒房,在打包啟動包之前,將這部分拆分出來的模塊的引用依賴添加到了啟動包中媚值,但是啟動包并不依賴這些引用情屹,所以App啟動時加載了本不需要加載的module;此外杂腰,項目還存在一個體積較大的老業(yè)務模塊垃你,由于手工拆分引用復雜,所以暫時也放在了啟動包中喂很,這也造成了啟動包過大的問題惜颇。
Webpack4自帶的SplitChunksPlugin插件實現(xiàn)了Bundle包之間的依賴管理,借助于這一工具少辣,可以方便地管理多Bundle之間的依賴關系凌摄,在分包的時候可以直接將entryPoint抽取出來,而entryPoint的依賴則由splitChunk去分析漓帅;而且SplitChunksPlugin提供的多種配置參數(shù)锨亏,為Bundle的管理提供更多的靈活性。
采用splitChunk進行分包忙干,分包體積總和由8063KB增加到8166KB器予,但啟動包體積占比由95%降低到了16%,啟動包加載時間降低了61%捐迫,啟動后在WebKit Malloc Zone上的resident size降低了約72%乾翔,在iPhone6 iOS 11.3真機測試中,內存降低了約85MB施戴。
splitChunk的分包與加載
splitChunk
先了解一下splitChunk的相關概念[1]:
- chunkGroup反浓,由chunk組成,一個chunkGroup可以包含多個chunk赞哗,在生成/優(yōu)化chunk graph時會用到雷则;
- chunk,由module組成肪笋,一個chunk可以包含多個module月劈,它是編譯打包后輸出的最終文件;
- module涂乌,就是不同的資源文件艺栈,包含了你的代碼中提供的例如:js/css/圖片等文件,在編譯環(huán)節(jié)湾盒,webpack會根據(jù)不同module之間的依賴關系去組合生成chunk湿右。
splitchunk是webpack4中的SplitChunksPlugin插件,webpack4使用SplitChunksPlugin插件來分析罚勾,先來看一下通過SplitChunksPlugins可以實現(xiàn)的功能毅人,對于如下a.js吭狡,b.js,c.js丈莺,d.js腳本:
// a.js
import add from './b.js
add(1, 2)
import('./c').then(del => del(1, 2))
// b.js
import mod from './d.js'
export default function add(n1, n2) {
return n1 + n2
}
mod(100, 11)
// c.js
import mod from './d.js'
mod(100, 11)
import('./b.js').then(add => add(1, 2))
export default function del(n1, n2) {
return n1 - n2
}
// d.js
export default function mod(n1, n2) {
return n1 % n2
}
當前設置splitChunk參數(shù)如下:
optimization: {
runtimeChunk: {
name: 'bundle'
}
}
如果以a.js為入口進行打包划煮,最后的分包結果如下所示:
上述4個腳本文件在編譯之后,生成如圖所示的結果:
- 生成了兩個chunkGroup缔俄,entryPoint和chunkGroup2弛秋;
- entryPoint這個chunkGroup只包含一個chunk,該chunk中包含a.js俐载,b.js和d.js這3個module蟹略;
- entryPoint依賴chunkGroup2,chunkGroup2只包含一個chunk遏佣,該chunk中包含c.js這個module挖炬。
最終結果就是a.js,b.js和c.js合并打包為bundle1状婶,c.js單獨打包為bundle2意敛,在進入entryPoint時,由于entryPoint依賴于chunkGroup2膛虫,所以需要先加載chunkGroup2的chunk草姻,即bundle2,然后再加載entryPoint的chunk走敌,即bundle1碴倾。
分包
在splitChunk編譯之后逗噩,可以得到chunkGroup之間的依賴關系掉丽,以及chunkGroup中的chunk的基本信息,其中"modules"字段為當前chunk所包含的所有module异雁。由于chunk是打包的最終輸出捶障,所以我們可以通過Metro對chunk包含的module信息進行打包。
// chunk中的module信息
{
"id": 0,
"modules": [
{
"id": 1,
"name": "./abc_test/b.js",
},
{
"id": 2,
"name": "./abc_test/d.js",
},
{
"id": 3,
"name": "./abc_test/a.js",
}
]
}
加載
加載entryPoint
React-Native的Bundle加載應該是以業(yè)務邏輯為單位的纲刀,所以加載時應該以entryPoint為單位项炼,而加載entryPoint則是通過加載其內部的chunks來實現(xiàn)的。
"entrypoint": {
"chunks": [
0
],
}
上述打包結果entryPoint只有1個chunk示绊,id為0锭部,所以就加載該chunk對應的bundle;當entryPoint包含多個chunk時面褐,按照順序從前往后加載chunk拌禾。
加載chunk
entryPoint之間的依賴關系體現(xiàn)在了chunk的"children"這一字段中,children里面是當前chunk所在的chunkGroup依賴的chunkGroup的chunks展哭,源代碼看起來更清晰一些:
const children = new Set();
const childIdByOrder = chunk.getChildIdsByOrders();
for (const chunkGroup of chunk.groupsIterable) {
for (const childGroup of chunkGroup.childrenIterable) {
for (const chunk of childGroup.chunks) {
children.add(chunk.id);
}
}
}
所以在加載chunk時需要將children中包含的chunk先加載進來湃窍,所以加載chunk是一個遞歸加載的過程闻蛀。如下所示,chunk 0依賴于chunk 1您市,所以需要先加載chunk 1觉痛,再加載chunk 0。
{
"id": 0,
"children": [
1
],
"modules": [
{
"id": 1,
"name": "./abc_test/b.js",
},
{
"id": 2,
"name": "./abc_test/d.js",
},
{
"id": 3,
"name": "./abc_test/a.js",
}
]
}
實驗
我們在打包之前茵休,先打一個引用react和react-native的包薪棒,包名為platformBase.ios.bundle。
// platformBase.ios.bundle
import 'react';
import 'react-native';
啟動包打包
在原方案中榕莺,由于一個老業(yè)務模塊的引用關系管理比較復雜盗尸,直接將這個3.7MB左右的老業(yè)務模塊包含到了啟動包中。此外帽撑,一些新模塊的引用被直接提取出來放在了啟動包中泼各,而這些依賴并不是啟動包必須引用的。
首先我們將老業(yè)務模塊的引用和新模塊依賴的引用從啟動包中刪除掉亏拉,然后把啟動入口JS文件作為entryPoint進行打包扣蜻,因為這是啟動包,我們也不需要使用splitChunk去提取公共引用及塘,直接將結果打在一個包中莽使。此時打包結果只有1個chunkGroup,內部包含1個chunk笙僚,將該chunk的打包結果記為0.ios.bundle芳肌。所以App在啟動時需要加載platformBase.ios.bundle和0.ios.bundle兩個包。
Bundle | 體積 |
---|---|
platformBase.ios.bundle | 645KB |
0.ios.bundle | 703KB |
經(jīng)過實驗測試肋层,依次加載兩個Bundle比合并起來加載要耗費更多的時間亿笤,所以我們將platformBase.ios.bundle和0.ios.bundle合并起來作為啟動包,記為merge.ios.bundle栋猖,體積為約為1.3MB净薛。
業(yè)務包打包
我們?yōu)槔蠘I(yè)務模塊創(chuàng)建一個模塊注冊入口頁,
import { AppRegistry } from 'react-native';
import BBB from '../xxx/pages';
AppRegistry.registerComponent('AAAA', () => BBB);
剩余的模塊入口頁保持不變蒲拉,將這些入口頁分別作為entryPoint肃拜,進行打包,
config.entry = {
xxxx_entry0: './xxxxx/entry0.js',
xxxx_entry1: './xxxxx/entry1.js',
xxxx_entry2: './xxxxx/entry2.ts',
xxxx_entry3: './xxxxx/entry3.ts',
xxxx_entry4: './xxxxx/entry4.ts'
},
同時配置splitChunk參數(shù)如下雌团,
splitChunks: {
minSize: 0,
cacheGroups: {
commons: {
name: 'commons',
chunks: 'all',
minChunks: 2,
priority: -20
}
}
}
目的是將這些入口模塊中引用至少2次的模塊抽取的commons里燃领,單獨作為一個chunk,單獨打一個Bundle锦援。這時需要注意猛蔽,在commons chunk中可能會包含啟動包merge.ios.bundle中已經(jīng)引用的module,所以在啟動包打包時雨涛,需要記錄下啟動包中包含的module枢舶,后續(xù)commons chunk在打包時需要過濾掉這些module懦胞。業(yè)務包打包結果如下:
Bundle | 體積 |
---|---|
0.ios.bundle | 2.3MB |
1.ios.bundle | 3.7MB |
2.ios.bundle | 364KB |
3.ios.bundle | 192KB |
4.ios.bundle | 135KB |
其中0.ios.bundle為業(yè)務模塊的公用依賴包,1.ios.bundle為老業(yè)務包凉泄,其他包為新的業(yè)務包躏尉。
結果分析
啟動包體積
原打包方案打包結果如下,
Bundle | 體積 |
---|---|
a.ios.bundle | 7.6MB |
b.ios.bundle | 41KB |
c.ios.bundle | 142KB |
d.ios.bundle | 98KB |
所有分包加起來體積為8063KB后众,其中a.ios.bundle作為啟動包胀糜,體積有7.3MB;而新的分包方案總分包加起來體積為8166KB蒂誉,其啟動包merge.ios.bundle體積僅有1.3MB教藻,體積縮小了82%。
App啟動Bundle加載時間對比
在iOS 11.3系統(tǒng)下iPhone6真機上測試啟動包加載時間右锨,兩種方案各進行5次測試括堤,原分包方案平均加載時間為4.17s,新分包方案平均加載時間為1.62s绍移,將加載時間降低了61%悄窃。
App啟動內存占用對比
在iOS 11.3系統(tǒng)下iPhone6真機上,原方案在App啟動后首頁露出physical footprint為155MB蹂窖,而新分包方案physical footprint為69MB轧抗,所以由縮小啟動包直接帶來了約85MB的內存優(yōu)化。
再通過iPhone XS iOS13.5模擬器查看App啟動后首頁露出時的Memory Graph對比瞬测,
Physical footprint對比
splitChunk分包 | 原分包方案 | |
---|---|---|
Physical footprint | 88.3M | 141.8M |
Physical footprint (peak) | 129.7M | 202.7M |
MALLOC ZONE對比
新分包方案
MALLOC ZONE | VIRTUAL SIZE | RESIDENT SIZE | DIRTY SIZE |
---|---|---|---|
DefaultMallocZone_0x1058fd000 | 128.0M | 9060K | 8948K |
MallocHelperZone_0x1058eb000 | 79.6M | 17.0M | 17.0M |
WebKit Malloc_0x1081d5000 | 26.0M | 21.4M | 20.2M |
QuartzCore_0x107620000 | 16.0M | 340K | 340K |
NWMallocZone_0x1081e1000 | 3072K | 40K | 40K |
TOTAL | 252.6M | 47.5M | 46.3M |
原方案
MALLOC ZONE | VIRTUAL SIZE | RESIDENT SIZE | DIRTY SIZE |
---|---|---|---|
DefaultMallocZone_0x109a9b000 | 128.0M | 9868K | 9668K |
WebKit Malloc_0x118105000 | 80.0M | 77.4M | 69.6M |
MallocHelperZone_0x1088a5000 | 79.6M | 16.8M | 16.7M |
QuartzCore_0x10b7bd000 | 16.0M | 348K | 348K |
NWMallocZone_0x1177d9000 | 3072K | 36K | 36K |
TOTAL | 306.0M | 104.2M | 96.2M |
從MACLLOC ZONE的角度來看横媚,新分包方案減少的內存主要集中在WebKit Malloc Zone,RESIDENT SIZE減少了約72%月趟。
總結
splitChunk可以構建React-Native分包之間的依賴關系灯蝴,并提供了更多的分包配置選項,靈活控制地Bundle的拆分狮斗,最終實現(xiàn)降低啟動Bundle的體積绽乔,加快App啟動的目的,并且減少App啟動時非必要的內存分配碳褒,提高App的存活幾率。