Skip to content

微信小程序分包与 uni-app 配置

结论先行

小程序分包的动机不是"代码组织",是微信的硬限制:单个分包/主包不超过 2MB,所有分包总和不超过 20MB。这两个数字是所有分包决策的边界条件,不是建议。

uni-app 的分包配置最终会被编译为微信小程序的 app.jsonsubpackages 字段——uni-app 在这里只是配置层的封装,没有改变微信小程序的分包机制本身。理解底层机制比记住配置格式重要。


微信小程序的分包机制

主包 vs 分包

text
启动流程:
  用户打开小程序
    → 下载主包(包含 app.js + 首页路由的页面文件)
    → 初始化 App + 首页 Page
    → 渲染首屏
    → 用户导航到分包页面时
      → 触分分包下载
      → 分包 JS 注入 JSCore 层
      → 分包页面渲染

主包:
  - 启动时必定下载
  - 包含 app.js / app.json / app.wxss
  - 包含 tabBar 页面(微信要求 tabBar 页面必须在主包)
  - 包含全局公共依赖(被多个分包引用的组件/JS)

分包:
  - 按需下载,首次进入该分包页面时触发
  - 下载后缓存在本地,后续进入不再下载
  - 每个包独立 2MB 限制

分包的加载是懒加载——用户不访问分包页面,该分包永远不会被下载。这意味主包体积直接影响启动速度(下载 + 初始化),分包体积影响首次进入该分包页面的等待时间。

分包预加载

preloadRule 允许在进入某个页面时提前下载其他分包:

json
{
  "subpackages": [
    { "root": "pages-sub/order", "pages": ["list/index"] }
  ],
  "preloadRule": {
    "pages/home/index": {
      "network": "all",
      "packages": ["pages-sub/order"]
    }
  }
}

规则含义:进入首页后,在网络空闲时(network: "all" 表示 WiFi + 4G 都预下载;"wifi" 表示仅 WiFi)预下载 order 分包。用户从首页进入订单页时,分包大概率已经下载完成。

但预加载不是免费的:提前下载消耗用户的流量和手机存储。只为"用户大概率会访问"的分包配置预加载。在数据上,如果首页 → 订单页的转化率是 40%,那 60% 的预下载是浪费的。

独立分包

json
{
  "subpackages": [
    {
      "root": "pages-sub/activity",
      "pages": ["index"],
      "independent": true
    }
  ]
}

independent: true 的分包不依赖主包。用户可以从小程序外(分享链接、二维码)直接进入独立分包页面,此时主包不会被下载——只下载该独立分包。

适用场景:活动落地页、临时性的营销页面。这些页面往往流量大但进入后转化路径短,不让它们拖累主包的下载。

代价:独立分包不能引用主包的任何资源(组件、JS 模块、样式)。这是一种高代价的隔离——意味着代码几乎不能复用,该分包内的资源需要完全自包含。


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": "订单详情" } }
      ]
    }
  ]
}

uni-app 编译到微信小程序时,这份配置被转换为 app.jsonsubpackages

json
// dist/build/mp-weixin/app.json
{
  "pages": ["pages/home/index"],
  "subpackages": [
    {
      "root": "pages-sub/order",
      "pages": ["list/index", "detail/index"]
    }
  ]
}

对应构建产物目录:

text
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.js    ← node_modules 公共依赖

分包预加载配置

uni-app 通过 preloadRule 实现:

json
{
  "preloadRule": {
    "pages/home/index": {
      "network": "all",
      "packages": ["pages-sub/order", "pages-sub/user"]
    }
  }
}

一个容易出错的地方:packages 数组里填的是分包的 root 值,不是页面路径。填成 pages-sub/order/list/index 不会生效。

公共依赖的归属问题

这是分包配置中最容易踩坑的点。uni-app 在编译时会将多个页面/分包共用的 JS 模块抽取到公共包中。但公共包的归属有两条规则:

  1. 被多个分包引用的模块 → 放入主包的 common/vendor.js
  2. 仅被单个分包引用的模块 → 保留在该分包内

这意味着:如果你为了让代码复用,把一个工具函数放在主包的 utils/ 目录下,然后多个分包引用它——没问题,它已经在主包里了。但如果你把工具函数放在分包 A 中,分包 B 引用它——uni-app 构建时会把这个模块提升到主包。结果是主包体积被悄悄增加,而你未必意识到。

排查方法:构建后看 dist/build/mp-weixin/common/vendor.js 的大小和内容。如果这个文件异常大,大概率是多分包共享的代码被广泛提升到了主包。此时选择:

  • 如果该模块确实被几乎所有分包使用 → 放在主包是合理的
  • 如果仅被 2-3 个分包使用 → 考虑各分包各自复制一份(牺牲维护性换主包体积),或评估是否可以将那 2-3 个分组合并为一个更大的分包

uni-app 条件编译的坑

uni-app 支持条件编译(#ifdef MP-WEIXIN),但条件编译发生在编译阶段,不改变分包边界。在一个分包内的 Vue 文件中使用条件编译排除某段代码,该代码的依赖仍然可能被 tree-shaking 前的分析包含进分包。实际影响:条件编译减的是运行时代码,不减的是编译时依赖分析引入的模块。如果条件编译排除的是一个完整页面,该页面的组件和依赖仍然可能出现在分包中——因为没有独立的分包级别的 tree-shaking。


实际分包决策框架

主包应该放什么

必须放(微信强制要求):

  • tabBar 的所有页面
  • app.js / app.json / app.wxss

应该放(工程判断):

  • 首屏页面(启动后用户看到的第一个页面)
  • 全局公共组件(<custom-navbar> 之类被大量页面使用的组件)
  • 公共工具函数和请求封装
  • 全局样式/主题变量

不应该放

  • 低频页面(如设置页、关于页、帮助页)
  • 仅被 1-2 个页面使用的重量依赖(如富文本编辑器、图表库)
  • 活动页/营销页(适合独立分包)
  • 体积大的图片和静态资源(放 CDN 引用)

分包粒度:宁愿少分大包,不要多分小包

一个常见误区:为了"架构清晰",按功能模块拆出 20 个分包,每个只有 50KB。结果是:

  • 用户在不同分包页面间跳转时,可能触发多次分包下载——每次下载都有网络延迟(RTT + 下载时间),即使每个包很小
  • 分包过多导致 app.jsonsubpackages 配置庞大,难以管理

更好的做法:合并关联页面。比如订单列表、订单详情、订单评价放在同一个 pages-sub/order 分包下——用户在这个路径下的跳转不需要额外下载。

作为参考,大多数 10-20 个页面的小程序,3-5 个分包足够;20-40 个页面的小程序,5-8 个分包足够。


一个实际案例的配置演变

下面是一个电商小程序的配置演变(隐私脱敏后的关键数据):

初始状态

text
主包: 2.4MB  ← 超限,无法上传
分包 order: 0.6MB
分包 user: 0.4MB
分包 product: 0.8MB

问题:主包超限。定位后发现主包中 vendor.js 占了 800KB——lodashmoment、以及一个图表库被多个页面引用,全部打进主包。

第一次调整

  1. momentdayjs(6KB vs 70KB),放在主包
  2. lodash → 按需引入(import debounce from 'lodash/debounce'),删除全量 import _ from 'lodash'
  3. 图表库放到 product 分包(仅商品分析页使用,其他页面不需要)

结果:

text
主包: 1.9MB  ← 合规但有风险

第二次调整

  1. 结算页(含复杂的表单校验库)从主包移到 order 分包
  2. 商品列表页(首页 tabBar 页)中的图片懒加载组件保留在主包,但图片处理逻辑移到 product 分包
  3. 配置 preloadRule:首页 → 预加载 order + product 分包

最终:

text
主包: 1.7MB
分包 order: 0.85MB
分包 user: 0.4MB
分包 product: 1.1MB

主包控制在 1.7MB 左右,预留了 300KB 的缓冲空间给后续迭代新增的主包功能。这个缓冲不是可选的——如果不预留,每一次往主包加新功能都可能触发"上线前发现超限"的紧急处理。


与其他小程序平台的差异

uni-app 的跨端特性意味着分包配置需要在不同小程序平台间翻译。但不同平台的分包限制不同:

平台主包限制总包限制单个分包限制
微信小程序2MB20MB2MB
支付宝小程序2MB20MB2MB
百度小程序2MB20MB2MB
字节小程序2MB20MB2MB
QQ 小程序2MB20MB2MB

各平台差异主要在:是否支持分包预加载、是否支持独立分包、分包路径的格式要求。uni-app 做了大多数的兼容层,但 independentpreloadRule 在不同平台上的支持程度不同——如果项目需要同时发 5 个平台,建议在配置前先对照 uni-app 的跨端兼容表确认这两个特性在各目标平台的可用性。


分包加载的运行时调试

微信开发者工具 → 详情 → 本地代码 → 代码依赖分析,可以看到每个分包的实际大小和模块构成。也可以通过 wx.getUpdateManager() 或在开发者工具中模拟分包下载来观察加载时序。

关键指标:

  • 主包下载 + 初始化耗时(影响启动体验)
  • 分包首次下载耗时(影响页面间跳转体验)
  • 分包缓存命中情况(二次进入是否还需要下载)

在真机上测试分包下载速度比模拟器重要——模拟器在同一台机器上运行,下载速度不反映真实网络条件。特别是 4G 弱网环境下(下行速度可能低至 200-500KB/s),一个 1MB 的分包下载耗时 2-5s,这段时间用户看到的是 loading。


一些实际判断

不要把"规避 2MB 限制"当成分包的全部意义。 如果主包 1.2MB,远低于 2MB 限制,技术上不需要分包。但如果你有 15 个低频页面(如各种设置子页面、帮助文档),把它们放在主包里意味着每个用户启动时都下载这些大概率不会访问的代码。分包优化用户的实际下载量,不只是帮开发者绕过限制。

vendor.js 是主包体积的第一杀手。 大多数主包超限问题不是业务代码太多,而是公共依赖被打进了 vendor.js。排查顺序:先看 vendor.js → 再看业务代码。建议定期跑 source-map-explorerwebpack-bundle-analyzer 看构建产物的体积构成。

npm 包的引入要评估"增益/体积"。 lodash 全量引入(~70KB gzipped)→ 用 lodash-es + tree-shaking 或直接在 utils/ 里自己写需要用到的函数。momentdayjs(体积差 10x)或直接用 Intl.DateTimeFormat(零依赖)。在处理分包体积时,少引入一个重量依赖的收益比优化 50 个页面的代码加起来还大。

条件编译不能替代分包。 #ifdef MP-WEIXIN 减少的是运行时代码,但它不影响构建时的依赖分析。一个大依赖即使通过条件编译不在运行时加载,如果 import 语句没有被 tree-shaking 识别为 dead code,它仍然会被打进主包。