微信小程序包体积优化:2MB 到 400KB 的实操记录
小程序开发有个绕不开的坎:主包体积限制 2MB。
平时开发到一半没感觉,等真机预览、提审的时候突然冒出来"包体积超限",然后开始一轮手忙脚乱的删东西——删图片、删组件、删字体,删到最后功能都不全了。
这篇记录一次真实的瘦身过程,不是理论清单,是每一步改了什么、省了多少、踩了什么坑。
一、先搞清楚:2MB 到底限制的是什么
这是最容易搞错的点。
限制的是「主包 + 分包总和」以外的层级,具体来说:
- 主包限制 2MB
- 整个小程序包(含所有分包)限制 20MB
- 单个分包限制 2MB
所以严格说,不是"整个包不能超 2MB",而是主包里的东西不能超 2MB。
这个区别很关键,因为很多人一看到"超了"就把所有东西塞进分包,结果分包也超。
先明确目标:主包只放"启动即需要"的东西。
二、第一刀:干掉主包里的静态资源
先说结论,这一步通常能省 300KB - 800KB,是性价比最高的。
检查 iconfont 有多臃肿
小程序的 iconfont 是最容易被忽略的体积杀手。
一个正常项目里,iconfont 文件可能包含几百个图标——但实际用到的可能只有二三十个。
排查方式:找到项目里的 .ttf / .woff 文件,直接看文件大小。如果超过 100KB,基本就是图标全量打包了。
解法:用 iconfont 的 Unicode 引用方式,生成的是字体文件 + CSS,字体文件只包含你订阅的图标。
或者更直接:用图片图标或者 SVG。如果你项目里图标数量不多(20 个以内),直接换成 SVG 内联反而更省,而且不受字体加载延迟影响。
检查图片有没有在主包里
项目里肯定有一堆图片,散落在各个目录里。只要被某个主包页面 import 或者写在样式里,它就进主包。
解法:所有图片资源都放 CDN 或者用云存储,主包里不存图片。
判断标准很简单:打开构建产物目录,看 assets 里有什么。这个目录里的东西就是主包资源。
我们项目当时的 assets 里有:
| 内容 | 体积 | 处理 |
|---|---|---|
| iconfont 字体 | 210KB | 改成 SVG 内联,删字体 |
| 占位图 | 45KB | 移到 CDN |
| 启动图 | 38KB | 移到 CDN |
| 未使用的 tabBar 图标 | 12KB | 删掉两个变体 |
合计省掉 305KB。
tabBar 图标的坑
tabBar 必须用本地图片(不能用网络图片),且所有 tab 页的图标都会进主包。
我们之前有 5 个 tab,每个 tab 两套图标(选中/未选中),一共 10 张图。有 2 个 tab 的图标其实长得一样,根本不需要两套。
解法:合并视觉上相同的图标,能用一张就不做两套。
如果 tab 超过 5 个,考虑改成"自定义 tabBar",可以把图标放到分包里。
三、第二刀:分包策略
处理完静态资源,如果还是超,就该分包了。
怎么划分分包
原则:按业务模块划分,不要按页面数量硬切。
我们当时的划分:
主包:首页、登录、我的、以及所有 tab 页
├── 分包 A:订单模块(订单列表、详情、评价)
├── 分包 B:商品模块(商品详情、规格选择、下单)
└── 分包 C:我的扩展(设置、地址簿、消息)
判断标准很简单:这个页面的功能,主流程会用吗?
- 会的(比如首页)→ 主包
- 不会的(比如订单历史)→ 分包
用户第一屏根本不会点到的页面,全在主包里就是浪费启动速度。
分包的两个硬限制
1. 分包不能引用主包外的资源
这个经常踩坑。分包页面里 import 主包里的组件是不允许的。
// ❌ 分包页面引用了主包外的组件
import { CustomButton } from '../../components/button'
// ✅ 正确做法:把公共组件放主包,或者放到每个分包里
我们的做法是:高频公共组件(Button、Empty、Loading)放主包,低频业务组件放对应分包。
判断依据:这个组件有几个页面在用?只有 2-3 个页面用又都在同一个分包内,就放分包里。
2. 分包首次进入有加载延迟
分包不是免费的——用户第一次点进那个 tab,会等几百毫秒加载分包。
所以分包不要切太细。我们试过切 6 个包,结果每个包都很小,切换时的等待反而更明显。
最终定在 3 个分包,体验和体积平衡得比较好。
异步分包
如果你有某个特别大的业务模块(比如直播、视频),可以用异步分包:
// 只在真正用到时才加载
const live = defineAsyncComponent(() => import('./live/index'))
配合分包配置里的 "lazyCodeLoading": "requiredComponents",能让启动更快。
但注意:异步分包用到的组件,不能在主包里被同步引用,否则会被打进主包,异步就白做了。
四、第三刀:干掉那些"顺手加的"东西
这一步没有技术含量,但效果出奇地好。
未使用的依赖
# 看依赖树,找出体积大的
npm ls --all --depth=10
我们当时发现一个没用到的日期处理库,带了一整套时区数据,78KB。
解法:定期检查依赖,别看到顺手就 npm install。项目跑了一年,依赖树里全是历史沉淀。
用 bundle-analyzer 类工具扫一下,找出没被引用的包。
polyfill
小程序端不需要那么多 polyfill。如果你的项目用了 core-js,检查下是不是全量引入的。
解法:按需引入,别全量打包。
sourcemap 和调试文件
这些应该只在本地保留,不要进构建产物。
// project.config.json
{
"setting": {
"packNpmManually": false,
"minifyWXSS": true, // 开启压缩
"minifyJS": true, // 开启压缩
"disableUseStrict": true
}
}
五、优化后的实际数据
贴一下这次的完整对照:
| 阶段 | 主包体积 | 主要动作 |
|---|---|---|
| 优化前 | 1.87MB | — |
| 清静态资源后 | 1.56MB | iconfont 转 SVG、图片移 CDN |
| 分包后 | 890KB | 划分 3 个分包,公共组件重新分配 |
| 清理依赖后 | 745KB | 删无用依赖、开启压缩 |
| 微调后 | 412KB | tabBar 图标合并、异步分包 |
从超限到 412KB,主包降到原来的 22%。
六、几个容易忽略的细节
1. 主包里的图片会被 base64 内联
小体积图片(< 8KB)在构建时会被转成 base64 塞进 CSS/CSSJS 里。
这意味着一个 5KB 的图标,实际可能让 CSS 涨了 7KB(base64 体积比原图大 33%)。
如果你的 CSS 莫名其妙很大,先去 dist 里搜一下有没有 data:image。
2. 自定义组件的分包归属
uni-app / Taro 这类框架,自定义组件在哪个分包,取决于它被谁引用。
如果主包引用了某个组件,那个组件就永远在主包,哪怕它只服务分包页面。
这一点很反直觉,但很关键——想彻底瘦身,要从主包的依赖链开始砍。
3. 别为了体积牺牲可维护性
我们删掉过一些"暂时没用但预留了"的代码,后来又要加回来,来回折腾。
建议:只删确定不用的,标了 TODO 或注释的先留着。几百 KB 换来的维护成本不划算。
七、写在最后
包体积优化的核心思路其实就一句:主包只放启动必需的东西。
围绕这个原则,具体做法可以很多——移资源、删依赖、合并图标、分包、按需加载。
但更重要的是:改完要真的量一遍。
微信开发者工具里有"代码质量分析"或者上传时会给体积报告,每次提交前看一眼,别等提审被拒了才回头找原因。
最后提一句,我们做完这次优化还有个附带好处:启动速度明显变快了。因为主包小了,首屏加载的资源少了。这个是意外收获——本来只是想解决超限,结果顺手优化了体验。
如果你的项目也卡在 2MB 上,可以按这个顺序试试:先查静态资源 → 再查依赖 → 最后才考虑分包。大部分项目前两步就能解决大半。