Skip to content
微信小程序分包机制与 uni-app 工程配置
概述
小程序对代码包体积有一套硬性限制:单个包(主包或分包)不超过 2MB,所有包的总大小不超过 20MB。这些限制来自微信平台的规定,并非推荐值,它构成了所有分包决策的边界条件。分包的动机首先是满足这一约束,其次才是通过按需加载减少用户的非必要下载。
uni-app 的分包配置最终会被编译为微信小程序 app.json 中的 subpackages 字段。uni-app 在这一层只提供了配置封装,并未改变微信小程序的分包机制本身。掌握编译映射关系比单纯记忆配置格式更可靠。
基本概念
主包
主包在小程序启动时必定下载。它必须包含:
app.js/app.json/app.wxss- 所有 tabBar 页面(微信要求 tabBar 页面必须位于主包)
- 被多个分包共同引用的公共组件或 JS 模块
主包的体积直接影响启动耗时,因为它包含了启动必须执行的代码和资源。
分包
分包在用户首次访问该分包内的页面时触发下载,下载后会被缓存,后续进入不再重复下载。每个分包独立遵守 2MB 的限制。分包采用懒加载策略——用户不进入分包页面,该分包永远不会被下载。因此分包体积影响的是首次跳转时的等待时间,而非启动性能。
独立分包
独立分包通过 independent: true 标记。这类分包不依赖主包,用户可以通过分享链接或二维码直接打开独立分包页面,此时主包不会被下载。适用于活动落地页等自身功能完整、流量大但转化路径短的临时性页面。
代价是独立分包不能引用主包中的任何资源(组件、JS 模块、样式),必须完全自包含。这是一种高成本的隔离。
分包预加载
preloadRule 配置允许在进入某个页面时,于网络空闲期间提前下载指定的分包。例如,进入首页后预下载订单分包,当用户浏览到订单页时,分包大概率已经下载完成,减少等待时间。
预加载并非没有成本:它会消耗用户的流量和存储。只为“用户大概率会访问”的分包配置预加载才有收益。若页面间转化率不高,预下载会造成浪费。
工作原理
小程序启动与分包加载的完整链路可以拆解为以下几个阶段。
启动阶段
- 用户打开小程序,客户端发起主包下载请求(.wxapkg)。
- 主包下载完成后,App 与首页 Page 的 JS 注入逻辑层(Service),页面模板与样式注入渲染层(WebView),完成首屏渲染。
分包下载与注入 3. 当用户通过 wx.navigateTo 等方式跳转到分包页面时,框架检查该分包是否已缓存。 4. 若未缓存,触发 HTTP 请求下载该分包的 .wxapkg 文件。下载利用平台网络层,优先级低于当前页面的数据请求。 5. 下载完成后,分包中的 JS 代码注入逻辑层:公共依赖(如已被提升到主包的模块)通过主包引用,分包自有的模块直接在逻辑层中执行,并注册分包页面与组件。 6. 同时,分包页面模板和样式注入渲染层。 7. 框架创建分包页面实例,执行 onLoad、onShow 等生命周期,渲染层完成初次绘制。
预加载时序 如果设置了 preloadRule,则在进入指定页面后,网络空闲时会提前执行上述第 4 步——下载分包包体。后续跳转时,第 4 步命中缓存,直接进入第 5 步。
公共依赖归属 构建阶段会进行依赖分析:被多个分包引用的模块会被提升到主包的 common/vendor.js 中,仅被单个分包使用的模块保留在该分包内。这一过程影响了分包体积和主包大小,也是主包超限时需要重点排查的方向。
与浏览器动态导入的对比
将小程序分包的加载链与浏览器中的动态 import() 作对比,有助于理解其特殊性。
- 加载单位:浏览器动态导入以 ES 模块为粒度,按需下载并执行 JS。小程序分包以“包”为单位,下载一个包含页面、组件、样式等全部资源的压缩包,需要平台层面解析和注入。
- 执行环境:浏览器动态导入的模块在主线程的同一 JS 执行上下文中运行,可直接访问全局作用域。小程序运行在双线程模型下,分包 JS 在逻辑层同一线程中按模块作用域执行,独立分包无法访问主包模块,无法操作渲染层的 DOM,页面渲染通过数据驱动。
- 依赖可见性:浏览器模块系统允许动态导入的模块通过
import访问已加载的模块,不受限制。小程序分包默认可以引用主包模块(只要不是独立分包),但主包模块必须被显式提升,否则分包无法引用;独立分包则完全隔离。 - 预加载机制:浏览器可以通过
<link rel="modulepreload">或自定义脚本提前请求模块,但缺乏平台级别的空闲时预下载策略。微信小程序的preloadRule集成了网络状态判断(仅 WiFi 或全部网络),直接与导航场景绑定。 - 失败处理:动态导入失败时可捕获 promise 错误,自行降级。小程序分包下载失败时平台会展示系统级错误提示,开发者没有内置的 JavaScript 异常捕获手段(只能通过全局错误监听获取,但无法阻止默认错误提示)。
综上,小程序分包不是简单的“按需加载 JS”,而是平台托管的静态资源包懒加载,它将页面、模板、样式打包在一起,并在双线程模型中分别注入。
基本用法
微信小程序原生配置
在 app.json 中声明分包:
json
{
"pages": [
"pages/home/index"
],
"subpackages": [
{
"root": "pages-sub/order",
"pages": ["list/index", "detail/index"]
}
],
"preloadRule": {
"pages/home/index": {
"network": "all",
"packages": ["pages-sub/order"]
}
}
}配置预加载时,packages 数组中填写的是分包的 root 值,而非页面路径。network: "all" 表示 WiFi 和移动网络下均执行预下载;设为 "wifi" 则仅在 WiFi 下预下载。
独立分包的声明方式:
json
{
"subpackages": [
{
"root": "pages-sub/activity",
"pages": ["index"],
"independent": true
}
]
}uni-app 配置
uni-app 在 pages.json 中通过 subPackages 字段配置:
json
{
"pages": [
{
"path": "pages/home/index",
"style": { "navigationBarTitleText": "首页" }
}
],
"subPackages": [
{
"root": "pages-sub/order",
"pages": [
{
"path": "list/index",
"style": { "navigationBarTitleText": "订单列表" }
},
{
"path": "detail/index",
"style": { "navigationBarTitleText": "订单详情" }
}
]
}
],
"preloadRule": {
"pages/home/index": {
"network": "all",
"packages": ["pages-sub/order"]
}
}
}编译为微信小程序后,生成的 app.json 将 subPackages 转换为原生的 subpackages,对应的构建产物目录结构如下:
dist/build/mp-weixin/
├── app.js
├── app.json
├── pages/
│ └── home/
│ └── index.{js,json,wxml,wxss}
├── pages-sub/
│ └── order/
│ ├── list/
│ │ └── index.{js,json,wxml,wxss}
│ └── detail/
│ └── index.{js,json,wxml,wxss}
└── common/
└── vendor.jscommon/vendor.js 包含了主包公共代码,通常由 node_modules 中的共享依赖以及被多个分包引用的模块构成。
示例:分包调整过程
一个电商小程序在初始阶段出现主包超限问题:
- 主包:2.4MB(超出 2MB 限制)
- 分包 order:0.6MB
- 分包 user:0.4MB
- 分包 product:0.8MB
定位后发现主包的 vendor.js 占据约 800KB,lodash、moment 以及一个图表库被多个页面引用,均被打入主包。
第一次调整
- 将
moment替换为dayjs(体积从约 70KB 降至 6KB),保留在主包。 - 将
lodash的全量引入改为按需引入(例如import debounce from 'lodash/debounce'),移除全量导入。 - 将图表库移至 product 分包,因为它仅被商品分析页使用。
结果:主包降至 1.9MB,刚好满足限制但缓冲空间很小。
第二次调整
- 将结算页面(含复杂表单校验库)从主包移至 order 分包。
- 首页 tabBar 页面中的图片懒加载组件保留在主包,但图片处理逻辑移至 product 分包。
- 在首页配置
preloadRule,预加载 order 和 product 分包。
最终:
- 主包:1.7MB
- 分包 order:0.85MB
- 分包 user:0.4MB
- 分包 product:1.1MB
主包预留了约 300KB 缓冲。如果没有预留,后续任何新功能被放入主包都可能在提审时再次触发超限。
从上述调整可以看出:主包体积的削减通常从 vendor.js 入手;将仅被部分页面使用的重量级依赖下放到对应分包,可以显著降低主包负担;预加载能够缓解分包体积增大带来的跳转延迟。
注意点
公共依赖与 vendor.js
多个分包共用的 JS 模块会被提升到主包的 common/vendor.js。当出现主包超限时,应优先检查 vendor.js 的大小和内容。若某个模块只被 2–3 个分包引用,但被提升到了主包,说明它被识别为公共依赖。此时可以评估:
- 如果该模块确实被大多数分包需要,保留在主包是合理的。
- 如果只被少数分包使用,可考虑在各分包中分别维护一份副本(牺牲维护性换取主包体积),或者将相关页面合并为一个更大的分包,使该模块只存在于一个分包内。
构建后可以直接查看 dist/build/mp-weixin/common/vendor.js 的体积,或使用 webpack-bundle-analyzer 分析模块构成。
分包颗粒度
按功能将页面拆分为过多小分包(例如 20 个分包,每个 50KB)并不一定带来好处。用户在多个分包间跳转时,可能连续触发多次下载,每次下载都有网络延迟。即便单个包很小,频繁的下载也会累积可见的等待时间。
通常,页面数在 10–20 的小程序,3–5 个分包足够;20–40 个页面时,5–8 个分包是相对常见的规模。关联度高的页面(比如订单列表、订单详情、订单评价)应当放在同一个分包中。
条件编译与依赖剥离
uni-app 的条件编译(#ifdef MP-WEIXIN)在编译阶段处理代码剔除,能否移除模块依赖取决于条件编译作用于什么级别的语法。
当条件编译直接包裹顶层 import 声明时,被导入的模块及其依赖链会完全从构建产物中移除:
javascript
// #ifdef MP-WEIXIN
import heavyModule from './heavy-module';
// #endif在这种情况下,heavy-module 的代码不会出现在任何包中。
但如果 import 声明位于条件编译外部,而仅对模块的调用代码进行条件编译,模块本身仍会被整体打入产物:
javascript
import heavyModule from './heavy-module';
// #ifdef MP-WEIXIN
heavyModule.run();
// #endif此时即便 run() 调用被剔除,heavyModule 及其依赖依然会打包进入分包。这是因为构建工具的依赖分析基于静态 import,不会因调用点的条件编译而改变模块的引入状态。
对于页面级别:如果在 pages.json 的 subPackages 中配置了页面路径,即使该页面文件内容全部被条件编译移除,构建工具仍可能生成一个空壳页面文件(包含 Page() 注册等),但该页面对应的组件依赖不会再被打入。要完全移除一个页面,除了使用条件编译清空页面代码外,还需要在 pages.json 中删除对应的页面路径配置。
预加载的成本
预加载会消耗用户的流量和存储。配置时应基于实际流量数据判断:如果从页面 A 到分包 B 的转化率很低,大部分预下载的资源会被浪费。network: "all" 会在移动网络下也触发预下载,这在弱网环境下可能占用带宽并影响当前页面的数据请求。
限制
不同小程序平台对分包的硬限制基本一致,但部分特性的支持存在差异。
| 平台 | 主包限制 | 总包限制 | 单个分包限制 |
|---|---|---|---|
| 微信小程序 | 2MB | 20MB | 2MB |
| 支付宝小程序 | 2MB | 20MB | 2MB |
| 百度小程序 | 2MB | 20MB | 2MB |
| 字节小程序 | 2MB | 16MB | 2MB |
| QQ 小程序 | 2MB | 20MB | 2MB |
差异主要体现在独立分包和分包预加载的支持程度上。通过 uni-app 同时发布到多个平台时,应先对照 uni-app 的跨端兼容表,确认 independent 和 preloadRule 在各目标平台是否可用,避免上线后功能降级。
