基于splitChunk的React-Native的分包與加載

摘要

對(duì)React-Native包進(jìn)行劃分是優(yōu)化App啟動(dòng)和內(nèi)存占用的關(guān)鍵處理步驟,為此提出了一種基于splitChunk的分包方式。對(duì)原始React-Native項(xiàng)目的多入口entryPoint進(jìn)行分包,而這些多入口entryPoint之間的共同依賴通過(guò)設(shè)置splitChunk配置來(lái)提取到新的bundle中,在加載一個(gè)entryPoint對(duì)應(yīng)的bundle時(shí),首先遞歸加載該bundle依賴的其他bundle,然后再加載entryPoint自身Bundle。使用splitChunk進(jìn)行分包管理,可以便捷地管理多個(gè)Bundle之間的依賴引用關(guān)系,保證在加載Bundle時(shí)僅僅加載當(dāng)前Bundle所依賴的模塊,避免加載多余模塊。實(shí)驗(yàn)結(jié)果表明,本方法能夠?qū)eact-Native包進(jìn)行合理劃分,并最小化App啟動(dòng)時(shí)基礎(chǔ)包的體積,提高App啟動(dòng)速度,并減少App啟動(dòng)時(shí)的內(nèi)存占用。

相關(guān)工作

React-Native在分包時(shí)主要工作集中在依賴管理,目前項(xiàng)目的分包方案只拆分出了一部分業(yè)務(wù)模塊,在打包啟動(dòng)包之前,將這部分拆分出來(lái)的模塊的引用依賴添加到了啟動(dòng)包中,但是啟動(dòng)包并不依賴這些引用,所以App啟動(dòng)時(shí)加載了本不需要加載的module;此外,項(xiàng)目還存在一個(gè)體積較大的老業(yè)務(wù)模塊,由于手工拆分引用復(fù)雜,所以暫時(shí)也放在了啟動(dòng)包中,這也造成了啟動(dòng)包過(guò)大的問(wèn)題。

Webpack4自帶的SplitChunksPlugin插件實(shí)現(xiàn)了Bundle包之間的依賴管理,借助于這一工具,可以方便地管理多Bundle之間的依賴關(guān)系,在分包的時(shí)候可以直接將entryPoint抽取出來(lái),而entryPoint的依賴則由splitChunk去分析;而且SplitChunksPlugin提供的多種配置參數(shù),為Bundle的管理提供更多的靈活性。

采用splitChunk進(jìn)行分包,分包體積總和由8063KB增加到8166KB,但啟動(dòng)包體積占比由95%降低到了16%,啟動(dòng)包加載時(shí)間降低了61%,啟動(dòng)后在WebKit Malloc Zone上的resident size降低了約72%,在iPhone6 iOS 11.3真機(jī)測(cè)試中,內(nèi)存降低了約85MB。

splitChunk的分包與加載

splitChunk

先了解一下splitChunk的相關(guān)概念[1]

  • chunkGroup,由chunk組成,一個(gè)chunkGroup可以包含多個(gè)chunk,在生成/優(yōu)化chunk graph時(shí)會(huì)用到;
  • chunk,由module組成,一個(gè)chunk可以包含多個(gè)module,它是編譯打包后輸出的最終文件;
  • module,就是不同的資源文件,包含了你的代碼中提供的例如:js/css/圖片等文件,在編譯環(huán)節(jié),webpack會(huì)根據(jù)不同module之間的依賴關(guān)系去組合生成chunk。
    splitchunk是webpack4中的SplitChunksPlugin插件,webpack4使用SplitChunksPlugin插件來(lái)分析,先來(lái)看一下通過(guò)SplitChunksPlugins可以實(shí)現(xiàn)的功能,對(duì)于如下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
}

當(dāng)前設(shè)置splitChunk參數(shù)如下:

optimization: {
    runtimeChunk: {
      name: 'bundle'
    }
  }

如果以a.js為入口進(jìn)行打包,最后的分包結(jié)果如下所示:


chunkGroup關(guān)系圖.png

上述4個(gè)腳本文件在編譯之后,生成如圖所示的結(jié)果:

  • 生成了兩個(gè)chunkGroup,entryPoint和chunkGroup2;
  • entryPoint這個(gè)chunkGroup只包含一個(gè)chunk,該chunk中包含a.js,b.js和d.js這3個(gè)module;
  • entryPoint依賴chunkGroup2,chunkGroup2只包含一個(gè)chunk,該chunk中包含c.js這個(gè)module。

最終結(jié)果就是a.js,b.js和c.js合并打包為bundle1,c.js單獨(dú)打包為bundle2,在進(jìn)入entryPoint時(shí),由于entryPoint依賴于chunkGroup2,所以需要先加載chunkGroup2的chunk,即bundle2,然后再加載entryPoint的chunk,即bundle1。

分包

在splitChunk編譯之后,可以得到chunkGroup之間的依賴關(guān)系,以及chunkGroup中的chunk的基本信息,其中"modules"字段為當(dāng)前chunk所包含的所有module。由于chunk是打包的最終輸出,所以我們可以通過(guò)Metro對(duì)chunk包含的module信息進(jìn)行打包。

// 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īng)該是以業(yè)務(wù)邏輯為單位的,所以加載時(shí)應(yīng)該以entryPoint為單位,而加載entryPoint則是通過(guò)加載其內(nèi)部的chunks來(lái)實(shí)現(xiàn)的。

"entrypoint": {
    "chunks": [
        0
    ],
}

上述打包結(jié)果entryPoint只有1個(gè)chunk,id為0,所以就加載該chunk對(duì)應(yīng)的bundle;當(dāng)entryPoint包含多個(gè)chunk時(shí),按照順序從前往后加載chunk。

加載chunk

entryPoint之間的依賴關(guān)系體現(xiàn)在了chunk的"children"這一字段中,children里面是當(dāng)前chunk所在的chunkGroup依賴的chunkGroup的chunks,源代碼看起來(lái)更清晰一些:

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時(shí)需要將children中包含的chunk先加載進(jìn)來(lái),所以加載chunk是一個(gè)遞歸加載的過(guò)程。如下所示,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",
        }
    ]
}

實(shí)驗(yàn)

我們?cè)诖虬埃却蛞粋€(gè)引用react和react-native的包,包名為platformBase.ios.bundle。

// platformBase.ios.bundle
import 'react';
import 'react-native';

啟動(dòng)包打包

在原方案中,由于一個(gè)老業(yè)務(wù)模塊的引用關(guān)系管理比較復(fù)雜,直接將這個(gè)3.7MB左右的老業(yè)務(wù)模塊包含到了啟動(dòng)包中。此外,一些新模塊的引用被直接提取出來(lái)放在了啟動(dòng)包中,而這些依賴并不是啟動(dòng)包必須引用的。

首先我們將老業(yè)務(wù)模塊的引用和新模塊依賴的引用從啟動(dòng)包中刪除掉,然后把啟動(dòng)入口JS文件作為entryPoint進(jìn)行打包,因?yàn)檫@是啟動(dòng)包,我們也不需要使用splitChunk去提取公共引用,直接將結(jié)果打在一個(gè)包中。此時(shí)打包結(jié)果只有1個(gè)chunkGroup,內(nèi)部包含1個(gè)chunk,將該chunk的打包結(jié)果記為0.ios.bundle。所以App在啟動(dòng)時(shí)需要加載platformBase.ios.bundle和0.ios.bundle兩個(gè)包。

Bundle 體積
platformBase.ios.bundle 645KB
0.ios.bundle 703KB

經(jīng)過(guò)實(shí)驗(yàn)測(cè)試,依次加載兩個(gè)Bundle比合并起來(lái)加載要耗費(fèi)更多的時(shí)間,所以我們將platformBase.ios.bundle和0.ios.bundle合并起來(lái)作為啟動(dòng)包,記為merge.ios.bundle,體積為約為1.3MB。

業(yè)務(wù)包打包

我們?yōu)槔蠘I(yè)務(wù)模塊創(chuàng)建一個(gè)模塊注冊(cè)入口頁(yè),

import { AppRegistry } from 'react-native';
import BBB from '../xxx/pages';

AppRegistry.registerComponent('AAAA', () => BBB);

剩余的模塊入口頁(yè)保持不變,將這些入口頁(yè)分別作為entryPoint,進(jìn)行打包,

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'
},

同時(shí)配置splitChunk參數(shù)如下,

splitChunks: {
    minSize: 0,
    cacheGroups: {
        commons: {
            name: 'commons',
            chunks: 'all',
            minChunks: 2,
            priority: -20
        }
    }
}

目的是將這些入口模塊中引用至少2次的模塊抽取的commons里,單獨(dú)作為一個(gè)chunk,單獨(dú)打一個(gè)Bundle。這時(shí)需要注意,在commons chunk中可能會(huì)包含啟動(dòng)包merge.ios.bundle中已經(jīng)引用的module,所以在啟動(dòng)包打包時(shí),需要記錄下啟動(dòng)包中包含的module,后續(xù)commons chunk在打包時(shí)需要過(guò)濾掉這些module。業(yè)務(wù)包打包結(jié)果如下:

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è)務(wù)模塊的公用依賴包,1.ios.bundle為老業(yè)務(wù)包,其他包為新的業(yè)務(wù)包。

結(jié)果分析

啟動(dòng)包體積

原打包方案打包結(jié)果如下,

Bundle 體積
a.ios.bundle 7.6MB
b.ios.bundle 41KB
c.ios.bundle 142KB
d.ios.bundle 98KB

所有分包加起來(lái)體積為8063KB,其中a.ios.bundle作為啟動(dòng)包,體積有7.3MB;而新的分包方案總分包加起來(lái)體積為8166KB,其啟動(dòng)包merge.ios.bundle體積僅有1.3MB,體積縮小了82%。

App啟動(dòng)Bundle加載時(shí)間對(duì)比

在iOS 11.3系統(tǒng)下iPhone6真機(jī)上測(cè)試啟動(dòng)包加載時(shí)間,兩種方案各進(jìn)行5次測(cè)試,原分包方案平均加載時(shí)間為4.17s,新分包方案平均加載時(shí)間為1.62s,將加載時(shí)間降低了61%。


啟動(dòng)Bundle加載時(shí)間比較.png

App啟動(dòng)內(nèi)存占用對(duì)比

在iOS 11.3系統(tǒng)下iPhone6真機(jī)上,原方案在App啟動(dòng)后首頁(yè)露出physical footprint為155MB,而新分包方案physical footprint為69MB,所以由縮小啟動(dòng)包直接帶來(lái)了約85MB的內(nèi)存優(yōu)化。

再通過(guò)iPhone XS iOS13.5模擬器查看App啟動(dòng)后首頁(yè)露出時(shí)的Memory Graph對(duì)比,

Physical footprint對(duì)比
splitChunk分包 原分包方案
Physical footprint 88.3M 141.8M
Physical footprint (peak) 129.7M 202.7M
MALLOC ZONE對(duì)比
新分包方案
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的角度來(lái)看,新分包方案減少的內(nèi)存主要集中在WebKit Malloc Zone,RESIDENT SIZE減少了約72%。

總結(jié)

splitChunk可以構(gòu)建React-Native分包之間的依賴關(guān)系,并提供了更多的分包配置選項(xiàng),靈活控制地Bundle的拆分,最終實(shí)現(xiàn)降低啟動(dòng)Bundle的體積,加快App啟動(dòng)的目的,并且減少App啟動(dòng)時(shí)非必要的內(nèi)存分配,提高App的存活幾率。

參考文獻(xiàn)

[1]: webpack系列之六chunk圖生成

最后編輯于
?著作權(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)書(shū)系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

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