Skip to content

小程序打包

小程序打包是将开发代码编译成可在微信客户端运行的包的过程。微信对小程序包大小有严格限制,合理规划分包是小程序性能优化的关键环节。

原理解析:小程序编译流水线

先理解打包的整体流程,后续的操作指南才好理解为什么这么做:

源码目录                    本地编译产物                  上传后台二次编译
┌──────────┐   开发者工具    ┌──────────┐   上传    ┌──────────────┐
│ app.json │ ──编译打包──→  │ 主包     │ ──────→  │ 分包后代码    │ ← 新客户端
│ pages/   │               │ 分包A    │          │ 整包兼容代码  │ ← 老客户端
│ packageA/│               │ 分包B    │          └──────────────┘
│ utils/   │               │ ...      │
└──────────┘               └──────────┘
  1. 本地编译:开发者工具根据 app.jsonpages + subpackages 配置,将源码编译成 JS/WXML/WXSS,并按分包配置切分出各个包
  2. 上传:将编译后的代码包上传到微信后台(此时已经是分包形态)
  3. 后台二次编译:微信后台为了兼容旧版本客户端,会额外编译一份"整包兼容代码"——把各分包路径合并到 pages 中,老客户端拿到的是完整包

所以开发者不需要担心低版本兼容问题,微信后台自动处理了。

包大小限制

限制项上限
整个小程序所有分包20M(小游戏 30M)
单个分包/主包2M

超过限制后无法上传,开发者工具会直接报错。

分包处理

原理解析:为什么需要分包

小程序启动时,微信客户端需要先从服务器下载代码包,然后初始化 JS 运行环境(逻辑层)和 WebView(渲染层),最后渲染首屏。如果所有代码都在一个包里:

  • 下载慢:代码包越大,首次下载耗时越长,用户白屏时间增加
  • 内存占用高:所有页面代码常驻内存,低端机容易 OOM

分包的核心思路:把代码拆成"启动必需"和"按需加载"两部分,启动时只下载主包,进入分包页面时再下载对应分包,减少首次加载体积。

操作指南:配置分包

第一步:打开 app.json,保留首页和 TabBar 页面在 pages 中,其余页面按业务模块划分到 subpackages

json
{
  "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

第三步:点击开发者工具 详情 → 代码依赖分析,查看主包和分包的大小分布,确认分包是否生效。

image-20240418112403362

image-20240418112434908

第四步:如果分包没有生效,检查以下几点:

  • subpackages 中的 root 路径是否存在且正确
  • 页面文件是否已移动到对应 root 目录下
  • pagessubpackages 中的页面路径是否重复声明

image-20240418142517134

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

image-20240418142550629

image-20240418150917041

打包原则

  • 声明 subpackages 后,将按 subpackages 配置路径进行打包,配置路径外的目录将被打包到主包中
  • 主包也可以有自己的 pages(即最外层的 pages 字段)
  • subpackage 的根目录不能是另外一个 subpackage 内的子目录
  • tabBar 页面必须在主包内

操作指南:分包路由跳转

分包页面之间的跳转和普通页面一样使用 wx.navigateTo,但路径必须带上分包的 root 前缀:

js
// 从主包跳转到分包页面
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使用)  → 主包   ← 注意:即使主包不用,也打入主包

操作指南:如何判断依赖被打到哪里

  1. 打开开发者工具 → 详情 → 代码依赖分析
  2. 展开主包/分包节点,可以看到每个模块的具体归属
  3. 如果发现不该进主包的模块进去了,说明有多个分包引用了它,或主包也引用了它

常见问题:分包 A 和分包 B 都 import 了同一个工具函数,这个函数会被打入主包。解决方案:

  • 如果工具函数很小(几KB),打入主包无所谓
  • 如果工具函数很大,考虑内联到各自分包中,或拆分成轻量接口

独立分包

原理解析:普通分包 vs 独立分包的加载差异

普通分包启动流程:
用户打开 → 下载主包 → 初始化主包 → 发现是分包页面 → 下载分包A → 渲染页面

                                         这里有等待

独立分包启动流程:
用户打开 → 下载分包A → 初始化分包A → 渲染页面

          跳过主包,更快

独立分包本质上是一个自包含的小程序单元,有自己的 JS 运行时,不依赖主包的 App 实例。

操作指南:配置独立分包

和普通分包配置一样,加上 independent: true

json
{
  "subpackages": [
    {
      "root": "packageA",
      "pages": ["pages/cat/cat"],
      "independent": true
    }
  ]
}

image-20240418151843533

验证独立分包是否生效:在独立分包页面的 JS 中尝试 const app = getApp(),如果打印出 undefined,说明该分包确实独立运行、没有加载主包。

独立分包的限制

不能把所有分包都设为独立分包,原因:

  1. 无法可靠获取全局状态:独立分包不依赖主包,getApp() 返回 undefined。只有在主包恰巧被其他途径加载后,才能拿到 App 实例——概率性问题,调试困难
  2. 不支持使用插件:独立分包中无法使用 app.jsonplugins 声明的插件
  3. 公共样式不生效app.wxss 不会注入到独立分包,需要自行引入样式

注意

如果独立分包需要共享数据,应使用 wx.setStorageSync / wx.getStorageSync 等本地存储方式,不要依赖 getApp().globalData

操作指南:独立分包中处理登录态

js
// 独立分包页面 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,指定在哪个页面触发预下载哪些分包

json
{
  "preloadRule": {
    "pages/index/index": {
      "network": "all",
      "packages": ["packageA"]
    },
    "packageA/pages/cat/cat": {
      "network": "wifi",
      "packages": ["packageB"]
    }
  }
}
  • key:触发预下载的页面路径,进入该页面时触发
  • networkall(不限制网络)或 wifi(仅 WiFi 下预下载,避免消耗用户流量)
  • packages:要预下载的分包 rootname(与 subpackages 中的对应),填 root 时预下载整个分包,填页面路径则预下载单个页面

推荐配置策略

场景配置
首页 → 详情页(高频路径)首页配置 network: "all",预下载详情分包
列表页 → 编辑页(低频)列表页配置 network: "wifi",仅 WiFi 预下载
非核心分包(体积大)不配置预下载,让用户手动触发

操作指南:验证预下载是否生效

  1. 开发者工具 → 调试器 → Network 面板
  2. 进入触发预下载的页面,观察 Network 是否有分包的下载请求
  3. 也可以在 详情 → 代码依赖分析 中查看分包状态

建议

同一个分包可以被多个页面配置预下载。如果分包已经被预下载过,后续配置不会触发重复下载,微信内部有缓存去重机制。

操作指南:Taro / uniapp 的分包配置

框架项目配置分包与原生思路一致,只是配置文件的格式不同。

Taro(app.config.js)

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)

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)不同。

打包优化建议

  1. 图片等静态资源放 CDN,不要打包到小程序包里 — 图片是包体积的大头,一张2x图动辄几百KB
  2. 公共组件和工具函数放主包,避免多个分包引用导致打入主包后体积更不可控
  3. 按业务模块划分分包,每个分包大小控制在 2M 以内,预留后续迭代空间
  4. 定期用代码依赖分析检查包体积,尤其版本迭代后容易出现体积膨胀
  5. 提交前检查:开发者工具 → 详情 → 本地代码 → 代码包大小,确保未超限
  6. 保持开发者工具最新:上传时工具会自动压缩混淆,新版工具通常有更好的压缩效果

常备不懈,才能有备无患