基于splitChunk的React-Native的分包与加载

摘要

对React-Native包进行划分是优化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在分包时主要工作集中在依赖管理,目前项目的分包方案只拆分出了一部分业务模块,在打包启动包之前,将这部分拆分出来的模块的引用依赖添加到了启动包中,但是启动包并不依赖这些引用,所以App启动时加载了本不需要加载的module;此外,项目还存在一个体积较大的老业务模块,由于手工拆分引用复杂,所以暂时也放在了启动包中,这也造成了启动包过大的问题。

Webpack4自带的SplitChunksPlugin插件实现了Bundle包之间的依赖管理,借助于这一工具,可以方便地管理多Bundle之间的依赖关系,在分包的时候可以直接将entryPoint抽取出来,而entryPoint的依赖则由splitChunk去分析;而且SplitChunksPlugin提供的多种配置参数,为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,在生成/优化chunk graph时会用到;
  • chunk,由module组成,一个chunk可以包含多个module,它是编译打包后输出的最终文件;
  • module,就是不同的资源文件,包含了你的代码中提供的例如:js/css/图片等文件,在编译环节,webpack会根据不同module之间的依赖关系去组合生成chunk。
    splitchunk是webpack4中的SplitChunksPlugin插件,webpack4使用SplitChunksPlugin插件来分析,先来看一下通过SplitChunksPlugins可以实现的功能,对于如下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参数如下:

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

如果以a.js为入口进行打包,最后的分包结果如下所示:


chunkGroup关系图.png

上述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加载应该是以业务逻辑为单位的,所以加载时应该以entryPoint为单位,而加载entryPoint则是通过加载其内部的chunks来实现的。

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

上述打包结果entryPoint只有1个chunk,id为0,所以就加载该chunk对应的bundle;当entryPoint包含多个chunk时,按照顺序从前往后加载chunk。

加载chunk

entryPoint之间的依赖关系体现在了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';

启动包打包

在原方案中,由于一个老业务模块的引用关系管理比较复杂,直接将这个3.7MB左右的老业务模块包含到了启动包中。此外,一些新模块的引用被直接提取出来放在了启动包中,而这些依赖并不是启动包必须引用的。

首先我们将老业务模块的引用和新模块依赖的引用从启动包中删除掉,然后把启动入口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

经过实验测试,依次加载两个Bundle比合并起来加载要耗费更多的时间,所以我们将platformBase.ios.bundle和0.ios.bundle合并起来作为启动包,记为merge.ios.bundle,体积为约为1.3MB。

业务包打包

我们为老业务模块创建一个模块注册入口页,

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参数如下,

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

目的是将这些入口模块中引用至少2次的模块抽取的commons里,单独作为一个chunk,单独打一个Bundle。这时需要注意,在commons chunk中可能会包含启动包merge.ios.bundle中已经引用的module,所以在启动包打包时,需要记录下启动包中包含的module,后续commons chunk在打包时需要过滤掉这些module。业务包打包结果如下:

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为业务模块的公用依赖包,1.ios.bundle为老业务包,其他包为新的业务包。

结果分析

启动包体积

原打包方案打包结果如下,

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系统下iPhone6真机上测试启动包加载时间,两种方案各进行5次测试,原分包方案平均加载时间为4.17s,新分包方案平均加载时间为1.62s,将加载时间降低了61%。


启动Bundle加载时间比较.png

App启动内存占用对比

在iOS 11.3系统下iPhone6真机上,原方案在App启动后首页露出physical footprint为155MB,而新分包方案physical footprint为69MB,所以由缩小启动包直接带来了约85MB的内存优化。

再通过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的拆分,最终实现降低启动Bundle的体积,加快App启动的目的,并且减少App启动时非必要的内存分配,提高App的存活几率。

参考文献

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

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 216,287评论 6 498
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 92,346评论 3 392
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 162,277评论 0 353
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,132评论 1 292
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,147评论 6 388
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,106评论 1 295
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,019评论 3 417
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,862评论 0 274
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,301评论 1 310
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,521评论 2 332
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,682评论 1 348
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,405评论 5 343
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,996评论 3 325
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,651评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,803评论 1 268
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,674评论 2 368
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,563评论 2 352