小程序打包
小程序打包是将开发代码编译成可在微信客户端运行的包的过程。微信对小程序包大小有严格限制,合理规划分包是小程序性能优化的关键环节。
原理解析:小程序编译流水线
先理解打包的整体流程,后续的操作指南才好理解为什么这么做:
源码目录 本地编译产物 上传后台二次编译
┌──────────┐ 开发者工具 ┌──────────┐ 上传 ┌──────────────┐
│ app.json │ ──编译打包──→ │ 主包 │ ──────→ │ 分包后代码 │ ← 新客户端
│ pages/ │ │ 分包A │ │ 整包兼容代码 │ ← 老客户端
│ packageA/│ │ 分包B │ └──────────────┘
│ utils/ │ │ ... │
└──────────┘ └──────────┘- 本地编译:开发者工具根据
app.json的pages+subpackages配置,将源码编译成 JS/WXML/WXSS,并按分包配置切分出各个包 - 上传:将编译后的代码包上传到微信后台(此时已经是分包形态)
- 后台二次编译:微信后台为了兼容旧版本客户端,会额外编译一份"整包兼容代码"——把各分包路径合并到 pages 中,老客户端拿到的是完整包
所以开发者不需要担心低版本兼容问题,微信后台自动处理了。
包大小限制
| 限制项 | 上限 |
|---|---|
| 整个小程序所有分包 | 20M(小游戏 30M) |
| 单个分包/主包 | 2M |
超过限制后无法上传,开发者工具会直接报错。
分包处理
原理解析:为什么需要分包
小程序启动时,微信客户端需要先从服务器下载代码包,然后初始化 JS 运行环境(逻辑层)和 WebView(渲染层),最后渲染首屏。如果所有代码都在一个包里:
- 下载慢:代码包越大,首次下载耗时越长,用户白屏时间增加
- 内存占用高:所有页面代码常驻内存,低端机容易 OOM
分包的核心思路:把代码拆成"启动必需"和"按需加载"两部分,启动时只下载主包,进入分包页面时再下载对应分包,减少首次加载体积。
操作指南:配置分包
第一步:打开 app.json,保留首页和 TabBar 页面在 pages 中,其余页面按业务模块划分到 subpackages:
{
"pages": [
"pages/index/index",
"pages/logs/logs"
],
"subpackages": [
{
"root": "packageA",
"name": "packA",
"pages": [
"pages/cat/cat",
"pages/dog/dog"
]
},
{
"root": "packageB",
"name": "packB",
"pages": [
"pages/apple/apple",
"pages/banana/banana"
]
}
]
}root:分包根目录,对应项目中的文件夹name:分包别名,预下载时引用用pages:分包内的页面路径,相对于 root 的路径,不要写完整路径
第二步:在项目根目录创建对应的文件夹,把页面文件移动进去:
项目根目录/
├── pages/ ← 主包页面
│ ├── index/
│ └── logs/
├── packageA/ ← 分包A
│ └── pages/
│ ├── cat/
│ └── dog/
├── packageB/ ← 分包B
│ └── pages/
│ ├── apple/
│ └── banana/
└── app.json第三步:点击开发者工具 详情 → 代码依赖分析,查看主包和分包的大小分布,确认分包是否生效。


第四步:如果分包没有生效,检查以下几点:
subpackages中的root路径是否存在且正确- 页面文件是否已移动到对应 root 目录下
pages和subpackages中的页面路径是否重复声明

配置正确后,代码依赖分析中主包体积会明显下降:


打包原则
- 声明
subpackages后,将按subpackages配置路径进行打包,配置路径外的目录将被打包到主包中 - 主包也可以有自己的 pages(即最外层的 pages 字段)
subpackage的根目录不能是另外一个subpackage内的子目录tabBar页面必须在主包内
操作指南:分包路由跳转
分包页面之间的跳转和普通页面一样使用 wx.navigateTo,但路径必须带上分包的 root 前缀:
// 从主包跳转到分包页面
wx.navigateTo({
url: '/packageA/pages/cat/cat'
})
// 分包之间跳转
wx.navigateTo({
url: '/packageB/pages/apple/apple'
})
// 跳转到 tabBar 页面(必须用 switchTab,且 tabBar 页面在主包)
wx.switchTab({
url: '/pages/index/index'
})注意
分包中不能使用 wx.switchTab 跳转到另一个分包的页面,只能跳主包的 tabBar 页面。
依赖打包规则
原理解析:依赖分析是怎么做的
小程序打包时,开发者工具会对每个 JS 文件做静态依赖分析(类似 webpack 的 AST 解析),从入口文件开始遍历 require / import 建立依赖图,然后决定每个模块打到哪个包:
分析入口(app.js)→ 遍历依赖树 → 标记模块归属 → 切分到各包核心约束只有一个:主包不能引用分包资源,分包可以引用主包资源。这个约束是单向的,决定了以下所有规则:
三条核心规则
规则一:主包和分包同时使用了一个依赖
主包不能引用分包资源,但分包可以引用主包资源。为了让两个包都能引用到,该依赖只能打入主包。
规则二:某个依赖只在分包中使用
打入当前分包,不会影响主包体积。
规则三:两个分包使用了同一个依赖或资源
分包 A 和分包 B 相互独立、不能互相引用,所以公共依赖只能打入主包。
依赖 A(仅主包使用) → 主包
依赖 B(仅分包A使用) → 分包A
依赖 C(主包+分包A使用) → 主包
依赖 D(分包A+分包B使用) → 主包 ← 注意:即使主包不用,也打入主包操作指南:如何判断依赖被打到哪里
- 打开开发者工具 → 详情 → 代码依赖分析
- 展开主包/分包节点,可以看到每个模块的具体归属
- 如果发现不该进主包的模块进去了,说明有多个分包引用了它,或主包也引用了它
常见问题:分包 A 和分包 B 都 import 了同一个工具函数,这个函数会被打入主包。解决方案:
- 如果工具函数很小(几KB),打入主包无所谓
- 如果工具函数很大,考虑内联到各自分包中,或拆分成轻量接口
独立分包
原理解析:普通分包 vs 独立分包的加载差异
普通分包启动流程:
用户打开 → 下载主包 → 初始化主包 → 发现是分包页面 → 下载分包A → 渲染页面
↑
这里有等待
独立分包启动流程:
用户打开 → 下载分包A → 初始化分包A → 渲染页面
↑
跳过主包,更快独立分包本质上是一个自包含的小程序单元,有自己的 JS 运行时,不依赖主包的 App 实例。
操作指南:配置独立分包
和普通分包配置一样,加上 independent: true:
{
"subpackages": [
{
"root": "packageA",
"pages": ["pages/cat/cat"],
"independent": true
}
]
}
验证独立分包是否生效:在独立分包页面的 JS 中尝试 const app = getApp(),如果打印出 undefined,说明该分包确实独立运行、没有加载主包。
独立分包的限制
不能把所有分包都设为独立分包,原因:
- 无法可靠获取全局状态:独立分包不依赖主包,
getApp()返回undefined。只有在主包恰巧被其他途径加载后,才能拿到 App 实例——概率性问题,调试困难 - 不支持使用插件:独立分包中无法使用
app.json中plugins声明的插件 - 公共样式不生效:
app.wxss不会注入到独立分包,需要自行引入样式
注意
如果独立分包需要共享数据,应使用 wx.setStorageSync / wx.getStorageSync 等本地存储方式,不要依赖 getApp().globalData。
操作指南:独立分包中处理登录态
// 独立分包页面 onLoad
Page({
async onLoad() {
// 不依赖 getApp(),直接从 storage 读取
const token = wx.getStorageSync('token')
if (!token) {
// 跳转到主包的登录页
wx.navigateTo({ url: '/pages/login/login' })
return
}
this.loadData()
}
})分包预下载
原理解析:分包的下载时机
小程序的分包加载是**懒加载(lazy load)**模式:
用户首次进入分包页面 → 检查是否已下载 → 没有 → 下载分包 → 注入代码 → 渲染
↑
这里产生白屏白屏耗时 = 下载分包 + 接口请求 + 视图渲染。弱网环境下分包下载可能占 2-5 秒。
预下载的原理:在用户还停留在当前页时,利用空闲带宽提前下载目标分包的代码包。当用户真正跳转时,代码已经在本地了,直接跳过下载步骤。
无预下载:点击跳转 → 下载 → 渲染 (白屏明显)
有预下载:浏览首页 → 后台下载 → 点击跳转 → 渲染 (秒开)预下载的分包代码下载后会存储在本地缓存中,不会注入和执行 JS,只是提前下载文件。等到用户真正跳转时才会执行注入和渲染。
操作指南:配置预下载
在 app.json 中配置 preloadRule,指定在哪个页面触发、预下载哪些分包:
{
"preloadRule": {
"pages/index/index": {
"network": "all",
"packages": ["packageA"]
},
"packageA/pages/cat/cat": {
"network": "wifi",
"packages": ["packageB"]
}
}
}key:触发预下载的页面路径,进入该页面时触发network:all(不限制网络)或wifi(仅 WiFi 下预下载,避免消耗用户流量)packages:要预下载的分包root或name(与 subpackages 中的对应),填 root 时预下载整个分包,填页面路径则预下载单个页面
推荐配置策略:
| 场景 | 配置 |
|---|---|
| 首页 → 详情页(高频路径) | 首页配置 network: "all",预下载详情分包 |
| 列表页 → 编辑页(低频) | 列表页配置 network: "wifi",仅 WiFi 预下载 |
| 非核心分包(体积大) | 不配置预下载,让用户手动触发 |
操作指南:验证预下载是否生效
- 开发者工具 → 调试器 → Network 面板
- 进入触发预下载的页面,观察 Network 是否有分包的下载请求
- 也可以在 详情 → 代码依赖分析 中查看分包状态
建议
同一个分包可以被多个页面配置预下载。如果分包已经被预下载过,后续配置不会触发重复下载,微信内部有缓存去重机制。
操作指南:Taro / uniapp 的分包配置
框架项目配置分包与原生思路一致,只是配置文件的格式不同。
Taro(app.config.js):
export default {
pages: ['pages/index/index'],
subpackages: [
{
root: 'packageA',
pages: ['pages/cat/cat', 'pages/dog/dog']
}
],
preloadRule: {
'pages/index/index': {
network: 'all',
packages: ['packageA']
}
}
}uniapp(pages.json):
{
"pages": [
{ "path": "pages/index/index" }
],
"subPackages": [
{
"root": "packageA",
"pages": [
{ "path": "pages/cat/cat" }
]
}
],
"preloadRule": {
"pages/index/index": {
"network": "all",
"packages": ["packageA"]
}
}
}注意 uniapp 中字段名是 subPackages(大写 P),与微信原生 subpackages(小写 p)不同。
打包优化建议
- 图片等静态资源放 CDN,不要打包到小程序包里 — 图片是包体积的大头,一张2x图动辄几百KB
- 公共组件和工具函数放主包,避免多个分包引用导致打入主包后体积更不可控
- 按业务模块划分分包,每个分包大小控制在 2M 以内,预留后续迭代空间
- 定期用代码依赖分析检查包体积,尤其版本迭代后容易出现体积膨胀
- 提交前检查:开发者工具 → 详情 → 本地代码 → 代码包大小,确保未超限
- 保持开发者工具最新:上传时工具会自动压缩混淆,新版工具通常有更好的压缩效果