这可能是最全的小程序包大小优化方案
小程序是一种不需要下载安装APP即可使用的应用,得益于小程序的轻量级特性,每个小程序包不超过2MB,用户扫一扫、搜一下即可打开应用,对于用户体验和获取新用户来说,成本大大降低。
但轻量级也让开发受到诸多限制,如何在极其有限的空间中满足业务需求,减少代码量大小,避免超限被限制上传,对于复杂应用的小程序开发者尤其需要关注。
背景
本文内容基于自己多年负责小程序的经验总结,我们的小程序从17年初上线一直维护到现在,包含了APP的绝大部分功能,目前代码包总和大小达到5.3MB,主包约为1.8MB,并有2次超限。可以说我们在主包大小优化上下了不少功夫。
小程序的限制
- 所有包大小总和不超过20MB
- 单个包大小不超过2MB
- 不限制包的数量
业务体验限制
小程序有5个TabBar,除了中间的TabBar是H5页面外,其他页面都是原生,由于使用的是原生TabBar,所以页面也都放在主包,除了这些一级页面外,还有公共的登录流程、网络服务、埋点服务、日志服务、状态管理、首页楼层组件、商品组件、工具函数等等公共模块。这些代码我们也很难迁移到其他子包。这就需要我们尽可能在技术上多做优化减少非必要代码量。
控制代码包大小主要的价值在于:
- 降低小程序下载时长和页面首次渲染耗时,降低用户流失率
- 减少下载流量和本地空间占用,提升用户体验
- 代码包超过2MB则无法上传,影响版本发布
解决方案
分包加载
最简单的方案其实就是分包,只需要调整页面目录和app.json即可。但问题在于像我们这种维护很久的小程序,功能和页面是一点一点加上去的,在最初并没有面临包大小上限的问题,一些次要页面也放在主包,并且这些页面路径已被做成小程序码物料、配置在公众号菜单或文章中、提供给友商,这就让我们面临一个尴尬的处境,我们不能随便改这些页面的路径。这怎么解决了?
我们所有的页面访问都有PV埋点,包含页面名、路径和场景值等信息。
以此我们统计这些页面的PV埋点,依旧场景值,按照是否有直接从外部打开的情况把这些页面分成三种
- 没有外部直接访问情况
- 很少有外部访问情况
- 有较多的外部方案并且场景较为广泛
对于第一种我们可以直接迁移到其他分包,修改内部跳转路径即可。
对于第二种和第三种,我们采取的策略是原有的页面保留,但是掏空内部作为“过渡页”,在onLoad是重定向到新的页面,这样虽然老页面没有迁移,但内容基本都是空的,体积很小。同时基于渠道来源,通知业务方逐步修改路径,直到某一天“过渡页”的访问量降低0并持续一段时间的,在删除“过渡页”。
通过上述方式,我们基本把历史遗留在主包的次要页面给迁移完成。
资源CDN化
除了代码以外,小程序中的图片、音频等媒体文件也会比较大,所以我们把除iconfront和比较重要的图片外都放到了CDN,大多数这些图片在首屏加载中都不会被使用到,打开小程序后在通过网络载入,这样我们有3~4MB的媒体资源都放到了CDN。
页面外置
页面外置其实方案有两种,一种是前面已经提到的把页面放到其他分包,另一种是页面H5化,H5页面通过webView来加载。在小程序中的H5体验流畅度上并不差,只是初试加载的时候比较慢,非核心页面可以考虑迁移为H5。
数据外置
有时候还有一部分数据不基本不变更的,没有通过接口下发,而是放在小程序代码包中,比如门店支持的城市数据等等。这些数据可以通过小程序的“数据预拉取”功能页放在CDN,在小程序冷启动的时候通过微信后台提前拉取。
JS和CSS压缩
代码压缩在H5项目中基本是标配,在小程序中我们也从一开始使用了代码压缩。小程序代码上传是自身也有代码压缩的功能。但是在编译代码时我们也做了一遍压缩。
小程序上传是压缩:

JS代码压缩使用的是uglify-es:
import * as UglifyJS from 'uglify-es'
import * as chalk from 'chalk'
export function uglifyJS(code: string, filePath: string): string {
const result = UglifyJS.minify(code)
if (result.error) {
console.log(chalk.red('压缩错误', `文件${filePath}`)
console.log(chalk.red(result.error))
return code
}
return result.code
}
CSS代码压缩使用的是csso
WXML压缩
WXML的压缩,我们最初试用过html-minifier,但使用起来还有些问题。WXML本质上也是XML,所以我们使用了minify-xml作为WXML的压缩工具,改工具会把代码压缩到一行,我们主包的代码在压缩WXML后大小减少了接近100KB,还是很可观的。
const minify = require('minify-xml').minify
let code = fs.readFileSync(filePath).toString()
fs.writeFileSync(outputPath, minify(code))
按需打包
最初我们小程序是使用gulp来打包,gulp打包和webpack打包有一点不同,webpack可以基于文件依赖只打包使用到的文件,而gulp不行,会把目录下的文件都进行打包。这就导致有不少已没在使用的组件、页面、文件被打包到dist目录。
所以我们后续自研了小程序的构建工具,大体的思路是通过app.json、app.js为入口文件,依次进行依赖分析遍历和编译使用到的文件。
通过app.json遍历所有包中的页面:
如果JS和WXS文件时把代码转化为抽象语法树(AST),通过AST找到“require”和“import”语句遍历引入的文件,逐次编译;
如果是JSON文件遍历usingComponents中的组件;
如果是CSS或者Less文件在通过正则表达式,查询样式文件的应用一次遍历;
如果是WXML文件需要先识别出WXS代码通过AST进行编译,其他的XML代码通过正则表达式找到其他引用文件依次遍历。
遍历引用文件替换为打包后的文件地址,在此过程中我们还可以支持通过NPM来安装JS、组件、样式等,我们也是通过这个方式让小程序支持使用NPM(没有使用微信的npm),整个过程较为复杂后续有机会可以在详细介绍。
通过依赖分析的方式进行编译打包再也不用担心会把无用代码文件也打包进入dist。
分包异步化
在小程序中,不同的分包对应不同的下载单元;因此,除了非独立分包可以依赖主包外,分包之间不能互相使用自定义组件或进行
require。「分包异步化」特性将允许通过一些配置和新的接口,使部分跨分包的内容可以等待下载后异步使用,从而一定程度上解决这个限制。
由于历史原因,还有部分比较老的商品和CMS楼层组件,在很少的场景下才会被展现,这种情况就特别使用使用“分包异步化”+“占位组件”的方式来把组件迁移出主包,放在分包中,在分包加载出来之前可以先展示占位组件,对用户的影响也比较小。
// 占位组件
// subPackageA/pages/index.json
{
"usingComponents": {
"button": "../../commonPackage/components/button",
"list": "../../subPackageB/components/full-list",
"simple-list": "../components/simple-list"
},
"componentPlaceholder": {
"button": "view",
"list": "simple-list"
}
}
跨分包JS代码引用
// subPackageA/index.js
// 使用回调函数风格的调用
require('../subPackageB/utils.js', utils => {
console.log(utils.whoami) // Wechat MiniProgram
})
// 或者使用 Promise 风格的调用
require.async('../commonPackage/index.js').then(pkg => {
pkg.getPackageName() // 'common'
})
不过分包异步化需要基础库版本 2.11.2 及以上,还有很少量的用户在使用该版本以下的微信。
代码依赖分析
有一段时间想要参考webpack-bundle-analyzer 来开发一个小程序版的工具,能够比较直观的查看每个包内的文件和大小、以及文件关系,正准备去调研是,发现小程序提供了这个工具。
挺好用,能帮我们比较直观的每个页面、组件、文件的大小,有了优化的方向和目标。

自定义TabBar
自定义TabBar也是一个方案,可以把主包中的页面精简到只有一个,我们有一段时间也测试了自定义TabBar,发现在有些时候,底部的TabBar会出现空白,在切换TabBar时也会出现闪现,对用户体验并不如在app.json中配置的默认TabBar友好,所以就放弃了该方案。
优化公共样式文件
我们的样式是使用Less来编写的,根据编译逻辑,公共的less文件中的代码在编译时会自动合并到每一个引用它的less文件中。也就是说,公共less文件看起来是写了一份,但编译后会成倍复制。
解决方法是对应公共样式代码尽量直接使用wxss,wxss编译后还是通过引用来导入公共样式文件,而不是合并其代码。

无用CSS检测
项目维护时间久后除了未使用的文件外,还有不少情况文件在使用,但是文件中的代码很多已不再使用,然后让开发一点一点排查有很难,谁都不敢轻易删除。
对于无用的CSS可以借助 PurgeCSS 来进行排查,PurgeCSS 我们正在调研如何引用到项目中。
总结
有一次参加地推一位老人使用较旧的安卓手机打开我们的小程序,硬是等了2、3分钟,那感觉像被抽了一巴掌。
所以对于大型小程序来说,推进主包体积优化很有必要,减少主包大小能提高用户打开的速度,这在每天呆在办公室里使用着WiFi的情况下可能并不明显。但是对于在外面使用者移动网络的用户来说很有必要。
介绍完这些方案和实践的思路,总的来说,就是尽量精简。
能压缩的就压缩,尽量只把最核心的内容放在主包,其他资源尽量放在分包或者CDN,在之后就是考虑使用技术手段剔除无用的文件和代码。
拙劣之处欢迎不吝赐教!