说个现实问题,微信小程序对主包体积限制一直是 2MB,后来放宽到 4MB,但主包越小越好,毕竟启动速度摆在那。但 uniapp 项目随便一搞,主包就冲上 1MB,再加上 uni_modules 里的库,一编译就是红色警告。我以前也被逼得实在没办法,花了一个周末把项目拆了个干净,主包从 900KB 压到 200KB 左右。今天把这套思路抖出来。
不是什么高深技术,就是分包、预加载、按需注入那点事,但你得真懂里面的门道,不然拆了照样白搭。
1. 先搞懂 uniapp 分包到底咋回事
uniapp 打包成小程序,pages.json 里的 subPackages 就是分包配置。每个分包可以有自己的根目录,独立打包,只有用到里面页面的时候才去下载。比如你的登录页、个人中心页可以扔到一个 subpackage-pages 里。
这里有四个关键点:
- 主包大小:包含 app.vue、main.js、公共组件、静态资源等,这部分会随小程序启动时全量下载。
- 分包大小:每个分包不是单独限制 2MB,而是所有分包加起来不能超过 20MB(微信现在很多类目可以 30MB)。
- 分包预下载:可以在启动后空闲时提前下载某个分包,让用户点进去时不用加载。
- 独立分包:这个更狠,可以完全不加载主包就跑起来。适合活动页、游戏页。
但很多人遇到的问题是:我把很多页面放到分包里了,主包体积还是很大。那就得看主包里到底还装了什么。
2. 找出主包里的大头
我用的工具很简单:HBuilderX 编译小程序,然后看微信开发者工具的“代码分析”。里面能清楚看到每个包的体积,还能按文件排序。我当时发现主包里最大的居然是 uni_modules 里的几个组件,还有自己的静态图片。
比如有个 uni_icons 字体文件,动不动就几百KB。很多 uniapp 项目其实只用到了其中几个图标,但全量打包了。解决办法是:把用到的图标单独复制到一个公共目录,或者直接用 iconfont,别引整个 uni_icons。
再比如 store 文件里不小心 import 了某个大模块,也会被打进主包。所以在 pages.json 里配了分包,但代码里你在主包里 import 了分包页面的组件,那也会把组件混进去。这个坑特别隐蔽。
我主要干了几件立竿见影的事:
- 把自定义组件里用到的 uni_modules 库,全部改成自己写的小组件。比如我只用到一个
uni-list,直接手写一个,少打包一堆。 - 所有图片都上 CDN,本地只放一个 loading 占位图。
- 清理 main.js 里没用的插件,比如我没用 vuex,就不用引入。
- tabBar 图标压缩成 WebP,每个 3KB 左右。
3. 拆包的边界:别什么页面都往分包塞
主包里的页面越少越好,但 tabBar 页面必须在主包中。微信规定 tabBar 页面不能放在分包里。所以你的 tab 页只能留在主包,这没办法。但 tab 页用到的子页面,比如列表详情、选择器,都可以塞进分包。
我当时把项目分成几个分包:
{
"pages": [
{ "path": "pages/index/index" },
{ "path": "pages/me/me" },
{ "path": "pages/order/order" }
],
"subPackages": [
{
"root": "pages-extra",
"pages": [
{ "path": "detail/detail" },
{ "path": "search/search" },
{ "path": "comment/comment" }
]
},
{
"root": "pages-activity",
"pages": [
{ "path": "coupon/coupon" },
{ "path": "point/point" }
]
}
]
}
注意 root 不能和主包里的目录同名。这里我把不常用的页面都扔到了 pages-extra 里。
这样分完以后,主包就是三个 tab 页 + 公共组件 + store,其他全在分包里。启动的时候主包下载量小了很多。
4. 用 preloadRule 做分包预下载
虽然分包是懒加载,但如果用户点击某个 tab 后跳转到详情页,那个详情页所在的分包可能还没下载完,然后就会白屏一下。解决办法就是预下载。
微信小程序提供了 preloadRule,可以配置某个页面加载完以后,预下载指定的分包。我在 uniapp 的 pages.json 里是这样写的:
{
"pages": [
{ "path": "pages/index/index" }
],
"subPackages": [
{
"root": "pages-extra",
"pages": []
}
],
"preloadRule": {
"pages/index/index": {
"network": "all",
"packages": ["pages-extra"]
}
}
}
这样用户一进入首页,空闲就下载 pages-extra 分包。等他从首页点进详情时,分包已经在本地了,秒开。注意 preloadRule 只能在应用启动后的页面里配置,不能配在分包页面里。
这个规则挺实用,但别滥用。如果每个页面都预下载所有分包,那和没有分包也差不多了。我一般只预下载用户最可能点击的下一个分包。
5. 独立分包:让活动页飞起来
如果你的项目里有那种从分享链接直接进入的活动页,强烈建议改成独立分包。独立分包的代码与主包完全隔离,加载的时候只下载独立分包本身,不用先下主包。这样分享出去的页面打开速度会快很多。
配置方法也很简单,只需要在分包配置里加一个字段:"independent": true。
{
"root": "pages-activity",
"pages": [
{ "path": "lottery/lottery" }
],
"independent": true
}
但独立分包访问主包的东西会有限制:它不能直接使用主包里的自定义组件,需要把组件也复制到独立分包里。我当时为这个折腾了半天,最后发现只要把活动页里用到的组件都放到独立分包目录下,就不报错了。其实这就是一个独立的迷你小程序,有点麻烦但值得。
6. 编译条件:能砍就砍
uniapp 支持条件编译,可以根据平台选择性打包代码。我项目里有些模块只在 H5 用,有些只在小程序用。以前怕麻烦没弄,后来发现只要加上条件注释,代码直接不进入小程序包,体积又小了一截。
比如我在 main.js 里引入一个统计插件,但只给 H5 用:
// #ifdef H5
import stats from '@/common/stats.js'
stats.init()
// #endif
在小程序编译时,这段代码直接没了。说真的,这个功能是 uniapp 最大的良心,一定要养成习惯。
另外,组件里也可以用 #ifdef MP-WEIXIN 来区分微信小程序和其他平台。这样有些微信专属的登录逻辑不会打进来。
7. 主包里的公共代码也要“瘦身”
去掉第三方库以后,主包里还有一个东西容易忽略:公共组件。很多页面都会引一个 common-card.vue,它在主包里。但这个组件里如果又 import 了别的组件,比如一个日期选择器,那这个日期选择器就算只有详情页用,也被算到主包公共代码里了。因为代码重复引用,webpack 不知道把它放哪,只能放在主包。
解决办法是使用分包优化:在 manifest.json 的 mp-weixin 配置里,有个 分包优化 开关,开启后 uniapp 编译时会把那些只被分包引用的资源尽量分到分包去。我用的 3.x 版本默认关闭,需要手动开启。
{
"mp-weixin": {
"optimization": {
"subPackages": true
}
}
}
开启之后,效果很明显。比如有一个图片只在分包页面里用,它就会被打到分包里,不会留在主包。
8. 别在 main.js 里一气乱 import
有个坑我印象深刻。为了省事,我在 main.js 里把所有公共组件都 Vue.component 注册了一下,想着全局随便用。结果每个组件都被打进了主包。后来我改成在需要的页面局部引入,主包体积立马降了不少。
如果你真的有特别高频的组件,可以当成 easycom 组件,uni-app 的 easycom 会自动按需加载。我后来把大部分组件都改成 easycom 了,主包只留几个核心的。
9. 最终数据
我拿我的项目举个例子,不是很夸张但可以参考初始状态:
- 优化前主包:981KB
- 优化后主包:212KB
- 分包代码:1.3MB
主包减少大概 78%。启动加载速度明显变快,之前冷启动偶尔会有几秒白屏,现在基本一点就开。果然“包小腰不疼”。
10. 检查你的静态资源
除了 JS,主包里的图片也是大头。尤其是 tabBar 里的图标,微信要求必须本地文件,没法走 CDN。但你可以用压缩工具压成很小的 WebP,图标基本 3KB 一个。还有那些分包里用到的图片,最好都上传 CDN,然后代码里写网络链接。本地图库一多,主包瞬间爆炸。
我写了个小脚本,检查编译后主包里的图片资源,超过 10KB 的全都标注出来,然后一个个处理。现在主包里基本没有超过 5KB 的本地图片了。
11. 注意分包的依赖传播
有时候你以为分好了包,但实际编译后,主包体积没变。原因是分包里的一些模块被主包引用了,导致 webpack 把这些模块提升到主包。比如我在 app.vue 的 onLaunch 里 import 了一个 utils/request.js,而 utils/request.js 又引用了 store/user.js。如果这个 store 只在分包页面里用,那就白搭了。
我的经验是:主包代码里不能直接引用分包页面里的模块或组件。要是公共模块,就放在主包的 utils 里,被多处使用;要是分包私有,放在分包自己的目录下,然后通过相对路径引用。
另外,我还在主包里用了 require.context 去看所有路由,结果它把分包页面也引用了,导致分包页面模块被拉进主包。这个比较坑,但解法很简单:别用 require.context 处理页面路由,老老实实写在 pages.json 里。
12. 在真机上测
开发工具里看体积是一个样,真机上跑又不一样。微信开发者工具可以看到“代码包大小”以及每个分包的大小。测试的时候最好关掉缓存,冷启动一次,看看主包加载时间和分包加载是否正常。
还有一个点:页面跳转时如果分包没下载完,会看到切换动画卡一下。通过 preloadRule 基本能解决,但也要在弱网环境下测一测,毕竟预下载有时候会被用户断网。
写在最后
这些招都是我在实际项目里踩坑踩出来的,不一定适用所有项目,但核心思路是相通的:把主包留给最核心的页面和依赖,其余的全部拆出去。uniapp 的好处是写一套代码,分包配置也是在 pages.json 里搞定,比起纯原生小程序,工作量还是少一些的。
以后如果遇到主包体积超标,别急着删代码,先看看是不是公共依赖被强行拉进了主包,再考虑把页面拆成独立分包。反正我现在每做一个新功能,第一反应就是这东西能扔分包就扔分包。
行了,这篇就到这,希望你们也能把主包控制得死死的。

