---
url: /articles/size-control.md
---

# 滴滴出行小程序体积优化实践 {#size-control-practice}

> 作者：[sky-admin](http://github.com/sky-admin)

## 概述 {#overview}

2019年下半年，为了将微信钱包/支付宝九宫格入口的滴滴出行迁移为小程序，团队对小程序进行了大量的功能升级与补全。在整个过程中也遇到并克服了一系列问题和挑战，其中包体积问题尤为突出。接下来全面介绍一下滴滴出行小程序在体积控制方面做的努力与沉淀。

## 背景 {#background}

微信对小程序包体积的要求是总体积不得超过12M，主包及单个分包体积不得超过2M。支付宝对于小程序包体积的计算方式虽和微信略有区别，不过整体也大同小异。

18年至19年初时，滴滴出行小程序里承载的业务只有网约车，且业务需求较少，在主包内都能够搞定。而在下半年时，为了将微信钱包/支付宝九宫格入口迁移至小程序，小程序开始新增诸如公交/代驾/车服/单车/顺风车等众多业务线，同时网约车的业务需求也要做全面的补齐，业务量和代码量一起爆炸式增长。

滴滴出行包含了丰富多样的出行业务，包含了快车/专车/出租车/豪华车/拼车/单车/代驾/顺风车/公交/车生活等众多业务线。整个滴滴出行小程序的最重要，使用最高频的页面是首页与订单详情页，首页中承载了各个业务线的需求表达，各个业务线的订单详情页则承载了具体的出行订单展示逻辑。此外还有各种功能页面比如个人中心，营销页面，设置，历史行程。

按照滴滴出行的产品逻辑，所有业务线的需求表达逻辑都在首页承载，为了良好的切换体验，在首页采用了单页顶导的方案进行业务线展示。即每个业务线在首页中提供一个需求表达组件，当用户切换顶导业务线后，切换出对应的业务线组件。

在这种设计下，所有的业务线的需求表达逻辑都集中在首页这个单一页面中，导致在业务迭代过程中，承载首页的主包体积迅速增长，很快触碰了小程序平台的单包2M上限，对后续的业务迭代与发展带来巨大阻碍。因此，对于包体积的控制是我们在小程序开发过程中面临的一大难题。

## 体积控制 {#size-control}

下面我们将介绍滴滴出行小程序开发迭代过程中，我们对于小程序包体积进行的一系列优化控制实践。

### 基础优化手段 {#basic-optimization}

对于小程序来说，基础的包体积优化手段包括：资源压缩/去除代码冗余/资源CDN化/异步加载

在web开发中，webpack提供了大量的代码优化能力，包括依赖分析、模块去重、代码压缩、tree shaking、side effects等，这些能力可以方便地完成资源压缩和去除代码冗余的工作。滴滴出行小程序基于滴滴开源的小程序框架Mpx( https://github.com/didi/mpx )进行开发，Mpx框架的编译构建完全基于webpack，兼容webpack内部生态，天然可以使用上述能力对包体积进行优化。

小程序中支持部分静态资源（如图像视频等）使用CDN地址加载，我们会尽可能将相关的资源压缩后放到CDN上，避免这部分资源对包体积的占用。

小程序场景下无法像web当中通过script标签便捷地进行异步加载，但是小程序平台后期纷纷支持了分包加载的方案来实现该能力，由于分包加载是小程序特有的技术规范，webpack无法直接支持，因此Mpx框架专门针对该技术规范进行了良好的适配支持，关于该能力的应用我们会在后文详细阐述。

除此之外，Mpx框架还针对小程序场景进行了许多包体积优化的适配工作，如尽可能减少框架运行时包体积占用（压缩后占用56Kb），对引用到的页面/组件按需进行打包构建，声明公共样式进行样式复用，分包内公共模块抽取等。

在Mpx框架的这些能力的支持下，基本不需要额外配置就能构建出一个经过初步优化的小程序包。

> 微信开发者工具选项里也有类似的"上传代码时自动压缩混淆"可勾选，但在开发者工具中上传代码时计算体积是直接计算的当前项目代码的体积，并不会依据压缩后的体积。因此，如果你使用原生小程序进行开发，你的source代码极有可能进行进一步的压缩以节省空间。

### 分析体积 {#analyze-size}

虽然框架已经提供了很多在体积控制方面的优化，但是随着业务迭代我们发现主包体积依然偏大。

在遇到主包体积偏大后，我们需要弄明白，主包里有哪些东西？它们为什么这么大？

使用原生小程序或者其他非基于webpack的框架进行开发的同学遇到这个问题后，可能只能去看硬盘上的文件大小。这样一来，各个模块的大小占比可能并不直观。而我们则可以借助 webpack-bundle-analyzer 这样一个webpack插件去做辅助分析。

比如这是一个使用Mpx框架编写的demo，通过 `npm run build --report` 就可以看到这样一个界面：

![体积分析图](https://dpubstatic.udache.com/static/dpubimg/3e488293-959e-4617-8187-69fdb532e9ab.jpg)

可以看到这个demo工程由 moment / lodash / socket-weapp / core-js 等第三方库组成。各个库的大小，相互依赖关系也能清晰地看出。

对于滴滴出行小程序也是能看到类似的图，能看到整个项目到底是由哪些代码组成。

另外，滴滴出行前端开发一直是采用“源码编译”的，可以让整个项目里公共的依赖可以实现仅有一份，一起共用。简而言之，也有助于减少项目代码体积。[相关资料：https://github.com/DDFE/DDFE-blog/issues/23 ](https://github.com/DDFE/DDFE-blog/issues/23)

要完美发挥源码编译的效果，需要上下游一起建立整套源码编译生态，比如主项目的依赖方在声明公用依赖时，就应该用peerDep或者devDep来声明一些公有依赖，这些共有依赖应该在主项目中统一声明，避免因版本不同装出两份公共依赖，那样反而会增大体积。由于滴滴出行小程序涉及业务线及团队众多，部分团队可能并不知道这个事情，因此代码里实际是可能出现上述劣化场景。而依照分析图，可以容易地发现这种问题，并推动相关团队清除这些重复依赖。

同时，我们依照体积分析图，对其中体积较大的文件重点分析，进行了一轮业务代码梳理和精简，删除了一些无用代码，精简了websocket的消息体描述文件等。

### 配置分包 {#configure-subpackages}

分包是小程序给出的类似web异步引入的一个方案，把一些初始进入时不需要的页面可以放进分包里，跳转到对应页面时才去下载分包，将这些页面及其附属资源放到分包里可以有效减少主包体积。

Mpx框架早期对分包规范进行了初步支持，资源访问规则保持和微信一致，主要根据资源存放的目录判断应该输出到主包还是分包。有这个能力后，我们把行程页抽到了分包，大概抽出了200多K左右的空间。

有了行程页的成功拆分后，我们开始对所有的非首页代码进行分包操作，比如起终点选择和个人中心。以及部分业务线的接入是通过npm的方式接入，我们也尽可能将这些业务线的所有非首页的代码放到了分包。

这里还有个题外话，得益于mpx早期设计了packages形式的业务组合方案，可以很方便地让业务独立开发，又能及其方便地整合。而后发现微信的分包的json配置设计和packages很像，就在这个基础上支持了微信的分包，用户侧仅需在原来的packages基础上加一个query标记这个分包的名字即可。

拆除各个分包后，整个项目结构大概如图：

![分包一期结构图](https://dpubstatic.udache.com/static/dpubimg/0562f94d-3929-42b8-a389-854e14983373.png)

初阶的分包工作进行完毕后，总计从主包里拆了差不多400K的空间到分包里。

### 分包资源精细化管理 {#subpackage-resource-management}

上面提到，Mpx框架初期的分包处理规则是完全按照微信的方式，把在分包路径下的资源收集到分包里。而npm管理的资源因为都在node_modules目录下，不属于任何分包路径，则会被全部收集进主包。

比如之前我们有行程页分包，行程页自有的状态管理store整个都在行程页分包的路径下，就会被收集到行程页分包中。而行程页还用到了封装好的didi-socket库，这个库是公共的npm包，即使它只在行程页分包里被使用，但由于它本身路径是在node_modules下的，那么就会将其收集进主包里。

因为早期的一些设计，行程页的资源和首页是分割开的，都比较独立地存在于各自的路径下，一期的分包处理的大头也主要是行程页，它刚好契合了Mpx初期对分包处理上的特点，因此能较好地收集进行程页分包里。

随着业务迭代，后续大量业务线的接入都是通过npm进行的，就会有大量npm包资源，他们都在node_modules目录下，因此全部会被收集进主包。

所以Mpx框架进行了一系列改造：

1. 在构建的依赖收集过程中，我们会对收集到的依赖打上标记，记录它是被哪些分包引入的。一旦它只有一个分包引入，它就会被输出到这个分包中。
2. 我们会根据用户定义的分包配置，自动在 SplitChunksPlugin 中生成各个分包的 cacheGroups ，把分包中的复用模块抽取到分包下的bundle中。
3. 对于组件和静态资源，如果他们被多个分包所引用且未在主包中引用，为了确保主包体积最优，这些资源将产生多份副本分别输出到对应分包中，而不会占用主包体积。

这样一来，***不管分包中引用的资源原本在什么位置，最终输出时都会尽可能将其输出到dist的分包目录下，避免占用主包空间***。

这个改动完成后项目结构看似和之前一样，但得益于Mpx处理分包资源能力的升级，我们得以将业务线分包中引用的npm资源成功输出到其所在的分包目录下。

### 封面方案 {#cover-solution}

再后来滴滴出行小程序需要替换微信/支付宝里原有的WebApp入口，小程序接入的业务线迅速增加，包体积迅速增长。

这个部分体积增长的主要原因前面提到过，所有的业务线都要接入到主页来展示。这也是由于业务特点决定的，滴滴出行提供了丰富的出行产品线，包括快车/专车/出租车/豪华车/拼车/单车/代驾/顺风行车等产品，用户是可能需要反复切换挑选的。这个过程还要保留起终点车型之类的信息，必须是一个页面内切换组件加一整套非常复杂的大型状态管理才能比较流畅顺滑地实现。而不能像一些电商/信息平台，将不同的功能拆分到不同页面，让用户通过首页的菜单进入子页面再进行操作，首页只承载入口，只有较少的业务逻辑，分包处理起来就会容易很多。

因此各个业务线都要提供首页组件进行接入。这个组件会在首页被用到，所以无论如何也拆不到分包里。最终，整个首页主包部分的体积可以分成两个部分：基础库和业务代码。两者的体积占比大概是公共依赖基础库占1M左右，业务代码占1M左右。

这么庞大的基础库体积主要是由于滴滴出行的业务线及业务团队众多，各方均有一些自己的基础依赖。比如网约车依赖的长链接通信pb数据描述文件，地图会依的大数计算库，顺风车依赖的CML框架运行时、代驾依赖的通信网关库，以及公用的组件库和polyfill等。

所以滴滴出行小程序面对的问题在当时已经无法用纯技术方案在短期内快速解决问题了，于是我们做了一个工程架构调整，可以叫封面页方案，解决了主包问题。

封面方案简单讲，就是做一个带滴滴出行Logo的封面作为启动页面，而页面一旦加载，立刻跳转另一个页面，这个页面真正承载业务，且它被放在分包里。

这个操作的意义在于，主包里就只剩下了所有方都要依赖的基础框架/库等，而业务全被抽离到了分包里。

![封面方案结构图](https://dpubstatic.udache.com/static/dpubimg/0f0ed782-8f67-4601-bc83-a8a343e1050b.png)

这是封面方案完成后项目的结构图，之前很大块的首页业务逻辑被抽出到首页分包中了。

这样一个挪移的操作的结果是我们可以有2M的主包空间来乘放基础的公共的库，有一个2M左右的分包来乘放前面提到的滴滴出行的集成了各种业务的“大主页”。而当时拆下来差不多有1.2M的主包，800K+的业务主分包。

这个改造最优秀的一点在于，后续的业务迭代产生的体积增长几乎全是在业务主分包里，剩下的1.1M+空间留给业务迭代还是比较充裕的。而主包的体积在理想条件下是可以长久保持不变的，就不会因为业务需求的不断开发反复导致主包体积临近超标，不再需要为主包体积感到焦虑。

当然，可以看到，这个方案本身是没有消减任何体积的，只是把位置变换了一下。除此之外，这个封面页方案其实也存在一些缺陷，比如首屏业务的展示会变慢，因为要加载的内容会变多，不过小程序本身有较好的缓存资源的能力，因此还算可以接受。

相比于因体积问题卡住需求迭代以及产品线的接入，目前这个方案至少能解决有无问题。我们开发团队后续也会持续跟进关注体积问题，看是否会有产品方案变更或者小程序本身给出一些解决方案来进一步优化这个部分。

## 总结 {#summary}

Mpx框架在包体积控制上做了大量工作，对于npm场景下的小程序分包进行了非常完善的支持。

滴滴出行小程序团队在框架支持的基础上，通过梳理业务依赖，充分利用分包，调整交互方案等一系列手段，在不阻碍业务发展的前提下，将庞大复杂的滴滴出行小程序包体积控制在平台限制范围内。

希望本文能给在包体积上遇到问题的小程序开发者们带来一些启发，欢迎留言交流。
