小程序分包加载与启动性能优化实战指南 2026
目录
小程序分包加载与启动性能优化实战指南 2026#
分包不是「把目录挪走」这么简单。它同时改变下载单元、注入范围、路由路径、资源归属和跨包依赖;如果只做机械拆分,常见结果是:主包依然接近 2M,进入业务分包时额外等待下载,甚至因为跨包引用而在真机白屏。本文把分包加载和启动性能放在同一条链路里处理:先明确启动阶段,再决定拆什么、预下载什么、注入什么、缓存什么,最后用同口径数据验证效果。
本文以微信小程序官方能力为主,并给出原生微信、Taro、uni-app 三套配置。所有平台版本、大小上限和基础库要求均来自文末官方文档;性能收益没有通用固定值,请按第 10 节的方法用自己的正式版、真机和真实用户路径量化。
一、先理解启动链路:优化点在哪里#
微信官方将小程序启动过程划分为运行环境准备、小程序信息准备、代码包准备、逻辑层注入、视图层注入、首页渲染,以及异步数据到达后的首屏内容展示。两个关键前提必须记住:
- 各阶段会尽可能并行,不能把每个阶段耗时简单相加来计算总启动时间。
- 框架层面以首页
Page.onReady触发作为启动过程完成。但如果首页主体内容依赖wx.request,onReady后仍可能出现业务意义上的白屏或骨架屏。
| 启动阶段 | 主要内容 | 开发者可控度 | 对应优化 |
|---|---|---|---|
| 运行环境准备 | 进程、WebView、JS 引擎、基础库等 | 基本由微信客户端控制 | 不宣称自己优化了该阶段;用真机分设备统计 |
| 小程序信息准备 | 版本、配置、权限等元信息缓存/更新 | 间接影响 | 合理规划发版,避免把每次实验都推给全量用户 |
| 代码包准备 | 下载、校验启动所需代码包 | 高 | 主包瘦身、分包、独立分包、预下载 |
| 逻辑层注入 | 配置、页面、组件、业务 JS 注入 | 高 | 分包、lazyCodeLoading、移除无用依赖 |
| 视图层注入 | WXML/WXSS 编译产物注入 | 中 | 降低首页结构复杂度、减少组件声明 |
| 首页渲染 | 初始 data 渲染并触发 onReady | 高 | 初始 data、初始渲染缓存、组件用时注入 |
| 首屏内容展示 | 异步请求返回并 setData | 高 | 骨架屏、请求预发起、失败兜底、关键路径拆分 |
因此,一份可执行的启动优化地图应该是:
- 下载更少:主包只保留启动页、tabBar 页和必要公共代码。
- 提前下载:对高转化后续分包使用
preloadRule,但控制网络和体积。 - 注入更少:启用
lazyCodeLoading: "requiredComponents",清理无用usingComponents。 - 更早可见:对稳定首屏使用
initialRenderingCache与骨架状态。 - 更早有内容:关键请求与页面初始化并行,异步内容有加载/失败状态。
- 可验证:包体积、
appLaunch、firstRender、路由耗时、业务内容时间分开记录。
反例:只把主包从 1.9M 拆到 1.2M,不处理全局组件和首页请求,用户看到有意义内容的时间可能没有明显变化。包体积是输入指标,不是唯一结果指标。
二、分包基础:主包、普通分包与官方边界#
2.1 打包单元#
微信小程序使用分包后,一定会包含一个主包。官方对主包的定位是:
- 默认启动页面;
- tabBar 页面;
- 所有分包都会用到的公共资源与 JS 脚本。
普通分包在用户进入其中页面时才下载。也就是说,分包优化的是「启动时需要下载的代码」,不是把所有代码都变成免下载。若某个分包是首页必经路径,只拆分而不预下载,启动体验未必变好。
官方大小限制:
| 限制项 | 上限 |
|---|---|
| 整个小程序所有分包合计 | 30M |
| 服务商代开发的小程序合计 | 20M |
| 单个主包或单个分包 | 2M |
这些是平台配置的硬边界。项目里更应该建立更严格的内部预算,例如「主包目标 ≤ X M」「启动必经分包 ≤ Y M」,具体阈值由业务基线和低端设备数据决定,而不是贴着 2M 上限发布。
分包能力要求微信客户端 6.6.0、基础库 1.7.3 及以上。独立分包、预下载、按需注入等能力有更高要求,见后续各节。
2.2 原生微信完整配置#
下面示例展示一个电商/交易类小程序的常见划分:
// app.json
{
"pages": [
"pages/home/index",
"pages/search/index",
"pages/profile/index"
],
"subpackages": [
{
"root": "package-trade",
"name": "trade",
"pages": [
"pages/order-list/index",
"pages/order-detail/index",
"pages/refund/index"
]
},
{
"root": "package-activity",
"name": "activity",
"pages": [
"pages/campaign/index",
"pages/coupon/index"
]
},
{
"root": "package-share",
"name": "share",
"pages": [
"pages/landing/index"
],
"independent": true
}
],
"preloadRule": {
"pages/home/index": {
"network": "all",
"packages": ["trade"]
},
"pages/search/index": {
"network": "wifi",
"packages": ["activity"]
},
"package-share/pages/landing/index": {
"network": "wifi",
"packages": ["__APP__"]
}
},
"lazyCodeLoading": "requiredComponents",
"window": {
"navigationBarTitleText": "MiniApp",
"navigationBarBackgroundColor": "#ffffff",
"navigationBarTextStyle": "black",
"backgroundColor": "#f6f7f9"
},
"tabBar": {
"color": "#667085",
"selectedColor": "#1652f0",
"backgroundColor": "#ffffff",
"list": [
{ "pagePath": "pages/home/index", "text": "首页" },
{ "pagePath": "pages/search/index", "text": "搜索" },
{ "pagePath": "pages/profile/index", "text": "我的" }
}
]
}
配置解读:
root是分包根目录,pages里的路径相对该root。name是别名,preloadRule.packages可以使用root或name。independent: true声明独立分包。__APP__表示预下载主包,常用于从独立分包回流主应用。preloadRule.network不配置时默认仅 Wi-Fi;all表示不限网络。lazyCodeLoading只写"requiredComponents",不要凭记忆写其他值。
2.3 拆分策略#
建议按「启动必要性 + 业务域 + 共享关系」三步拆:
- 启动必要性:首页、tabBar 页、登录兜底、全局错误页、极小公共 shell 留主包。
- 业务域:交易、活动、内容编辑、设置、低频工具按域拆分,避免一个
pages-sub变成新的巨型包。 - 共享关系:只有主包确实使用的依赖放主包;仅某个分包使用的大依赖移动到该分包;多个分包共同依赖的模块要比较「放主包」「复制到各包」「分包异步化」的体积与加载成本。
目录示意:
miniprogram/
├── app.js
├── app.json
├── app.wxss
├── components/
│ └── shell/ # 多包共用,保留主包前先确认是否真被主包使用
├── pages/
│ ├── home/
│ ├── search/
│ └── profile/
├── package-trade/
│ ├── components/order-card/
│ ├── services/order.js
│ ├── assets/empty-order.png
│ └── pages/order-detail/
├── package-activity/
│ ├── components/coupon-card/
│ └── pages/campaign/
└── package-share/
├── app-shell.wxss # 独立分包不能依赖 app.wxss,需有自己的基础样式
└── pages/landing/
拆分后必须更新三类引用:
- 页面跳转:
/package-trade/pages/order-detail/index?id=1; - 组件路径:分包页面 JSON 里的
usingComponents; - 静态资源:图片、字体、WXS 等应跟随唯一使用方,不要全部留在根目录。
三、跨包引用与分包异步化#
普通分包的引用规则必须当作架构边界管理:
- 分包 A 不能直接
require分包 B 的 JS; - 分包 A 不能引用分包 B 的 template、资源、自定义组件;
- 普通分包可以引用主包和自身内部内容;
- tabBar 页面必须在主包内。
如果确实需要跨分包复用,优先考虑以下顺序:
- 下沉主包:模块很小且主包也使用,可以放主包。
- 复制轻量模块:极小纯函数复制到各包,换取加载独立性,但要接受维护成本。
- 服务端聚合:跨业务状态由后端接口统一,避免客户端全局时序假设。
- 分包异步化:基础库 2.11.2+ 支持异步引用其他分包 JS/组件/插件,适合低频大依赖。
跨分包 JS 示例:
// package-trade/pages/order-detail/index.js
function renderRefundPanel(mod) {
// 使用异步加载到的模块渲染面板
}
require.async('../../package-refund/index.js')
.then(renderRefundPanel)
.catch(({ mod, errMsg }) => {
// 上报加载失败,并展示本包兜底 UI
reportLoadError({ mod, errMsg });
});
跨分包组件需要 componentPlaceholder:
// package-trade/pages/order-detail/index.json
{
"usingComponents": {
"refund-panel": "../../package-refund/components/refund-panel/index",
"refund-placeholder": "../components/refund-placeholder/index"
},
"componentPlaceholder": {
"refund-panel": "refund-placeholder"
}
}
这不是常规复用方案,而是「低频大模块不拖慢主包」的折中。占位组件必须有明确尺寸与可访问状态,否则用户会感知到界面跳变。
四、独立分包实战:适合分享落地页,不适合所有页面#
独立分包的基础要求是微信客户端 6.7.2、基础库 2.3.0+。它的最大价值是:从独立分包页面进入小程序时,不需要先下载主包。适合:
- 分享落地页;
- 活动/邀请页;
- 独立工具页;
- 异常恢复页;
- 广告或外部投放承接页。
不适合:
- 依赖全局登录态、主题、埋点 SDK、请求封装的页面;
- 需要自定义 tabBar 或大量主包组件的页面;
- 生命周期里立即读取
getApp().globalData的页面; - 未验证插件与运行时依赖的页面。
4.1 getApp() 兼容#
从独立分包冷启动时,App 可能尚未注册,getApp() 可能是 undefined。基础库 2.2.4+ 支持 allowDefault:
// package-share/pages/landing/index.js
function createAppState() {
return {
scene: '',
inviteCode: '',
bootstrapReady: false,
};
}
Page({
data: {
loading: true,
inviteCode: '',
},
onLoad(query) {
// 独立分包不能假设主包 App 已经存在。
const app = getApp({ allowDefault: true }) || {};
app.shareBootstrap = createAppState();
app.shareBootstrap.scene = query.scene || '';
app.shareBootstrap.inviteCode = query.invite || '';
this.setData({
inviteCode: app.shareBootstrap.inviteCode,
loading: false,
});
},
});
更稳妥的做法是独立分包内部有自己的 bootstrap.js:先完成本包渲染和必要请求,再在用户主动进入主包时同步状态。不要把主包的全局初始化副作用复制一份后假设两边执行顺序一致。
4.2 样式与插件边界#
- 主包
app.wxss对独立分包无效; App只能在主包定义,独立分包中不能定义App;- 独立分包不能依赖主包和其他分包的 JS、template、WXSS、自定义组件、插件等内容;启用分包异步化后,JS、自定义组件、插件有对应异步能力,仍需逐项验证;
- 官方文档提示独立分包暂不支持使用插件,项目落地前应以当前基础库和工具诊断为准,不要默认可用。
工程上建议给独立分包一个最小样式入口,例如 package-share/app-shell.wxss,只包含落地页所需的变量、按钮和布局,避免复制整个 app.wxss。
五、分包预下载:用转化率换流量#
preloadRule 的作用是在进入某页面时自动预下载后续可能需要的分包。基础库要求 2.3.0+。它适合解决「主包启动很快,但用户点击后还要等分包下载」的断层。
5.1 规则设计#
{
"preloadRule": {
"pages/home/index": {
"network": "all",
"packages": ["trade"]
},
"pages/profile/index": {
"network": "wifi",
"packages": ["activity", "settings"]
},
"package-share/pages/landing/index": {
"network": "wifi",
"packages": ["__APP__"]
}
}
}
推荐策略:
- 核心下一步:首页到交易分包转化极高,可考虑
all,但必须持续观察包大小。 - 可选下一步:活动、设置、编辑工具默认 Wi-Fi。
- 低频功能:不预下载,首次进入时用明确的页面级 loading 兜底。
- 独立分包回流:落地页核心动作完成后,可 Wi-Fi 预下载主包,不建议落地页一出现就抢网络。
进入页面后,可在 vConsole 中查看以 preloadSubpackages 开头的日志验证是否触发。
5.2 预下载限制与流量核算#
同一个分包中的页面共享 2M 预下载大小限额,并由工具打包时校验。例如页面 A 预下载 0.5M,同包页面 B 最多还剩 1.5M。packages 里写多个包时,要核算合计体积,而不是只看单个包。
不要把所有分包都挂到首页:
- 会增加用户流量和存储占用;
- 会与首页关键请求抢网络;
- 首页弱网时可能拖慢可感知体验;
- 预下载不代表注入完成,不能消掉目标页面的 loading 设计。
基础库 2.27.3+ 还有 wx.preDownloadSubpackage 可用于特定交互后的显式预下载;这只应作为精细化补充,参数和使用限制以官方 API 页为准,不能替代 preloadRule 的配置治理。
六、启动渲染与注入优化#
6.1 lazyCodeLoading:先做,但必须清理依赖#
在 app.json 中配置:
{
"lazyCodeLoading": "requiredComponents"
}
基础库 2.11.1+ 支持。开启后,小程序只注入当前访问页面所需的页面和自定义组件代码;未访问页面、当前页面未声明的组件不会加载和初始化。
常见误解是「配置一行就结束」。实际上,页面 JSON 中声明的所有组件和 app.json 全局 usingComponents 都会被视为依赖。因此要同步做:
- 删除页面中已不再使用的
usingComponents; - 避免全局声明低使用率组件;
- 将组件声明移动到真正使用它的页面;
- 检查模板动态渲染和抽象节点是否仍能解析;
- 回归测试所有入口,尤其是分享进入、扫码进入、订阅消息进入。
插件包和扩展库目前不支持按需注入。若插件体积大,可考虑放入分包并通过分包异步化异步引入。
6.2 用时注入与占位组件#
基础库 2.11.2+ 支持在开启按需注入后使用「用时注入」:为自定义组件配置占位组件,组件第一次渲染前不注入;第一次渲染时先展示占位,再注入并替换。
{
"usingComponents": {
"editor": "../heavy-editor/index",
"editor-skeleton": "../editor-skeleton/index"
},
"componentPlaceholder": {
"editor": "editor-skeleton"
}
}
适用场景:
- 富文本编辑器;
- 图表和大屏可视化;
- 地图复杂覆盖物;
- 客服/直播/上传等低频重组件。
不适用场景:
- 首屏关键按钮;
- 需要立即可交互的表单控件;
- 占位组件高度不稳定导致明显布局抖动的区域。
6.3 initialRenderingCache:第二次进入更早可见#
页面配置:
{
"initialRenderingCache": "static"
}
基础库 2.11.1+ 支持。它会在页面第一次打开后记录「页面初始 data」的渲染结果,页面第二次打开时先直接展示缓存,不等逻辑层初始化完成。
它适合:
- 导航壳;
- 稳定骨架;
- 固定标题与背景;
- 首屏中不依赖异步请求的结构。
必须注意:
- 不包含后续
setData的结果; - 展示缓存时页面暂时不能响应事件,等逻辑层初始化完成后才可以;
- 缓存可能因小程序更新、基础库更新、存储回收等原因被清除;
- 缓存阶段复杂组件有限制,官方支持的内置组件包括
view、text、button、image、scroll-view、rich-text;自定义组件可出现,但其中的内置组件也要遵循限制。
正确写法是把首屏骨架状态放进初始 data:
// pages/home/index.js
Page({
data: {
shellReady: true,
listStatus: 'loading', // loading | success | empty | error
},
});
<!-- pages/home/index.wxml -->
<view class="home-shell">
<view class="home-title">精选商品</view>
<view wx:if="{{shellReady}}" class="home-skeleton">
<view class="skeleton-row" />
<view class="skeleton-row" />
<view class="skeleton-row short" />
</view>
</view>
不要在 onLoad 里先 setData({ shellReady: true }),那不会进入静态初始渲染缓存。基础库 3.7.4+ 另支持 capture 自定义缓存时机,可用于特定场景,但必须验证低版本回退表现。
七、自定义组件与公共 chunk 治理#
分包之外,最常见的包体积问题是「组件和依赖被公共化」:
- 全局
usingComponents声明了大量组件; - 构建器把仅分包使用的依赖提取到主包;
- UI 库整包引入;
- 图表/编辑器/地图 SDK 被多个页面同步引用;
- 图片和字体被放在根目录,被工具归入主包。
原生项目的检查顺序:
- 导出
app.json与所有页面 JSON 的usingComponents,统计实际引用; - 用构建产物或包体积分析确认每个大文件归属;
- 只被一个分包使用的组件移动到该分包;
- 多包共用且主包也使用的组件留在主包,但做按需声明;
- 跨分包低频重组件用占位组件和分包异步化;
- 大图上传 CDN,包内只保留必要占位图。
八、Taro 完整配置示例#
Taro 的分包配置最终会编译到微信小程序 app.json。下面是一个 React/Taro 项目示例:
// src/app.config.js
export default {
pages: [
'pages/home/index',
'pages/profile/index',
],
subpackages: [
{
root: 'package-trade',
pages: [
'pages/order-list/index',
'pages/order-detail/index',
],
},
{
root: 'package-activity',
pages: [
'pages/campaign/index',
],
},
{
root: 'package-share',
pages: ['pages/landing/index'],
independent: true,
},
],
preloadRule: {
'pages/home/index': {
network: 'all',
packages: ['package-trade'],
},
'package-share/pages/landing/index': {
network: 'wifi',
packages: ['__APP__'],
},
},
lazyCodeLoading: 'requiredComponents',
window: {
navigationBarTitleText: 'MiniApp',
navigationBarBackgroundColor: '#ffffff',
navigationBarTextStyle: 'black',
},
};
跳转路径必须带分包根目录:
import Taro from '@tarojs/taro';
export function openOrderDetail(orderId: string) {
return Taro.navigateTo({
url: `/package-trade/pages/order-detail/index?id=${orderId}`,
});
}
Taro 3.3.11+ 提供智能提取分包依赖,可把主包未依赖、分包独占的模块提取到分包,减少主包体积:
// config/index.js
const config = {
mini: {
optimizeMainPackage: {
enable: true,
},
},
};
module.exports = config;
该能力当前只支持微信小程序,官方说明支持 .wxss、.json、.js、.wxml、.wxs。注意它会为多个分包共同依赖的模块生成 sub-common 并复制到对应分包,主包变小但总包可能变大。开启后必须同时比较:
- 主包大小;
- 启动分包大小;
- 总包大小;
- 进入各分包的路由耗时;
- 低版本基础库兼容。
如果使用独立分包,不要假设 Taro 运行时、主包公共模块、全局样式和插件都可用。以编译产物 dist/app.json 和真机冷启动路径为准。
九、uni-app 完整配置示例#
uni-app 在 pages.json 中配置页面与分包,在 manifest.json 的 mp-weixin 节点配置微信平台差异项:
// pages.json
{
"pages": [
{
"path": "pages/home/index",
"style": { "navigationBarTitleText": "首页" }
},
{
"path": "pages/profile/index",
"style": { "navigationBarTitleText": "我的" }
}
],
"subPackages": [
{
"root": "package-trade",
"pages": [
{
"path": "pages/order-list/index",
"style": { "navigationBarTitleText": "订单" }
},
{
"path": "pages/order-detail/index",
"style": { "navigationBarTitleText": "订单详情" }
}
]
},
{
"root": "package-activity",
"pages": [
{
"path": "pages/campaign/index",
"style": { "navigationBarTitleText": "活动" }
}
]
},
{
"root": "package-share",
"pages": [
{
"path": "pages/landing/index",
"style": { "navigationBarTitleText": "邀请" }
}
],
"independent": true
}
],
"preloadRule": {
"pages/home/index": {
"network": "all",
"packages": ["package-trade"]
},
"package-share/pages/landing/index": {
"network": "wifi",
"packages": ["__APP__"]
}
},
"globalStyle": {
"navigationBarBackgroundColor": "#ffffff",
"navigationBarTextStyle": "black",
"backgroundColor": "#f6f7f9"
}
}
// manifest.json
{
"name": "miniapp",
"versionName": "1.0.0",
"mp-weixin": {
"appid": "wx-app-id",
"setting": {
"urlCheck": true,
"es6": true,
"minified": true
},
"lazyCodeLoading": "requiredComponents"
}
}
uni-app 官方说明:
subPackages在 H5 不支持;preloadRule支持微信、QQ、抖音、支付宝、京东小程序;- 微信小程序的 appid 等信息配置在
mp-weixin节点,不要放到app-plus。
页面跳转:
uni.navigateTo({
url: '/package-trade/pages/order-detail/index?id=1001',
});
构建后应检查 dist/dev/mp-weixin/app.json 与 dist/build/mp-weixin/app.json:
subPackages/subpackages是否生成;preloadRule是否保留;lazyCodeLoading是否在生成的app.json顶层;- 独立分包是否意外引用了主包公共样式或组件;
- 静态资源是否被编译进主包。
十、性能测量:先定口径,再看数据#
没有基线的性能优化很容易变成「感觉更快」。推荐把指标分为输入指标、框架指标和业务指标。
10.1 建立基线#
每次对比至少固定以下条件:
| 条件 | 要求 |
|---|---|
| 版本 | 尽量使用正式版;开发版/体验版链路更慢,不能直接外推 |
| 基础库 | 同一基础库版本 |
| 设备 | 至少一台低端 Android 与一台 iOS;分别记录 |
| 网络 | Wi-Fi 与弱网分开;记录网络类型 |
| 缓存状态 | 首次冷启动、第二次启动、版本更新后分开 |
| 入口 | 首页、分享落地页、扫码路径、消息路径分开 |
| 样本 | 每个口径保留多样本,看中位数、P75、P90,不只看平均值 |
记录表建议:
| 指标 | 来源 | 口径 |
|---|---|---|
| 主包大小 | 构建产物/上传结果 | 压缩后包体积 |
| 各分包大小 | 构建产物/上传结果 | 压缩后包体积 |
| 预下载合计 | preloadRule + 构建产物 | 同源包 2M 限额内合计 |
appLaunch | wx.getPerformance navigation entry | 启动耗时 |
route | wx.getPerformance navigation entry | 页面路由耗时 |
firstRender | wx.getPerformance render entry | 首次渲染 |
首页 onReady | 页面生命周期打点 | 框架启动完成 |
| 内容可见 | 首屏关键请求回调/骨架替换 | 业务可感知时间 |
| 交互可用 | 关键按钮可点击打点 | 用户可行动时间 |
10.2 wx.getPerformance 采集示例#
// utils/performance.js
const METRICS = new Set(['appLaunch', 'route', 'firstRender']);
const records = [];
function toRecord(entry) {
if (!METRICS.has(entry.name)) return;
records.push({
name: entry.name,
entryType: entry.entryType,
startTime: entry.startTime,
duration: entry.duration,
path: entry.path,
raw: entry,
});
}
export function collectPerformance(report) {
const performance = wx.getPerformance();
const observer = performance.createObserver((entryList) => {
entryList.getEntries().forEach(toRecord);
report(records);
});
observer.observe({ entryTypes: ['navigation', 'render'] });
// observer 注册太晚可能错过早期 entry,导出前再读取一次全量列表。
performance.getEntries().forEach(toRecord);
return {
records,
disconnect: () => observer.disconnect(),
};
}
在 app.js 的 onLaunch 尽早初始化,并把版本、机型、系统、基础库、网络、入口场景一起上报。注意:
- 不要只上报
duration,缺少设备与场景的数据无法归因; - 不要把一次本地测试当作结论;
- 不要把
onReady等同于用户看到内容; - 不要比较不同基础库、不同网络、不同缓存状态下的数据。
10.3 开发者工具 Audits 与体验评分#
微信开发者工具调试器的 Audits/体验评分适合做发布前静态与交互检查:
- 使用接近正式包的构建产物;
- 选择目标基础库;
- 清理无关调试面板与日志;
- 执行真实路径:冷启动首页、点击核心分包、返回、弱网、失败重试;
- 查看 Performance / Experience / Best Practices 分类;
- 把每条问题映射到责任人与修复版本,而不是只记录总分。
体验评分适合发现「不符合平台最佳实践」的问题;启动耗时归因仍要结合 wx.getPerformance、真机和线上采样。不要把一次 Audits 分数当作性能回归的唯一门禁。
10.4 量化优化效果#
建议报告使用同口径相对变化,并保留样本量:
实验 ID: package-split-2026-08-15
设备: Android low-end / iPhone mainstream
基础库: 3.x 实际版本
版本: 正式版 baseline vs treatment
入口: 首页冷启动 / 分享落地页冷启动
主包大小: baseline X KiB -> treatment Y KiB
总包大小: baseline X KiB -> treatment Y KiB
appLaunch P75: baseline X ms -> treatment Y ms
firstRender P75: baseline X ms -> treatment Y ms
内容可见 P75: baseline X ms -> treatment Y ms
核心分包路由 P75: baseline X ms -> treatment Y ms
失败率: baseline N% -> treatment N%
如果主包明显下降但「内容可见」没有改善,优先检查:
- 首页是否仍同步注入大量组件;
- 请求是否在
onLoad之后才发起; - 骨架是否依赖异步
setData; - 首屏数据是否过大;
- 分包是否没有预下载;
- 统计口径是否混入不同缓存状态。
十一、十个高频踩坑与修复#
坑 1:把 tabBar 页面移进分包#
现象:配置校验失败或 tab 无法展示。
原因:微信要求 tabBar 页面必须在主包内。
修复:tab 页、默认启动页和极小错误兜底保留主包;tab 内的二级功能再拆分包。
坑 2:跳转路径没有带 root#
现象:页面不存在或路由失败。
原因:分包页面完整路径是 root + pages 内路径。
修复:集中维护路由常量,禁止在页面里散落字符串拼接。
export const ROUTES = {
orderDetail: (id) => `/package-trade/pages/order-detail/index?id=${id}`,
campaign: '/package-activity/pages/campaign/index',
};
坑 3:图片看似移动,实际仍在主包#
现象:目录挪了,主包体积几乎不变。
原因:构建工具按引用和打包路径计算归属,未引用的大资源或根目录静态资源可能仍进入主包。
修复:以构建产物为准;大图走 CDN;分包专属资源放到分包目录;删除未引用文件。
坑 4:分包之间直接 import#
现象:开发者工具可能侥幸通过,真机或发布构建失败。
原因:普通分包不能直接引用其他分包 JS、template、资源、组件。
修复:下沉主包、复制轻量模块、服务端聚合,或使用分包异步化并设计失败兜底。
坑 5:独立分包假设 getApp() 一定存在#
现象:分享冷启动白屏或脚本报错。
原因:独立分包启动时主包和 App 可能尚未加载。
修复:使用 getApp({ allowDefault: true }) 并设计本包 bootstrap;关键逻辑不依赖主包副作用。
坑 6:把所有分包都挂到首页预下载#
现象:主包变小,首页弱网更慢,用户流量增加。
原因:预下载会占网络和存储,且同源分包共享 2M 限额。
修复:按转化率分级:核心下一步 all,可选下一步 Wi-Fi,低频不预下载。
坑 7:忽略预下载 2M 共享限额#
现象:单包都小于 2M,打包校验仍失败。
原因:同一分包中的页面共享 2M 预下载大小限额,不是每个规则独立 2M。
修复:建立 preloadRule 体积表,合并计算;调整 network 与触发页面。
坑 8:开启 lazyCodeLoading 但不清理组件声明#
现象:配置生效,启动数据没有变化。
原因:页面 JSON 与全局 usingComponents 中声明的组件仍会被注入。
修复:删除未使用声明,组件下沉到真正页面,避免全局声明低频重组件。
坑 9:把异步 setData 当成初始渲染缓存#
现象:第二次启动仍看不到骨架。
原因:static 初始渲染缓存只记录页面初始 data 的渲染结果,不包含后续 setData。
修复:骨架默认值写进 data;请求成功后再切换状态。
坑 10:框架公共 chunk 把分包依赖塞进主包#
现象:业务页面已分包,主包依旧超限。
原因:Taro/uni-app 的公共依赖提取策略把多个模块合并到 vendors/common。
修复:Taro 3.3.11+ 开启 optimizeMainPackage;uni-app 检查构建产物和依赖引用;重构整包引入的 UI/图表 SDK。
十二、发布前 10 项检查清单#
| # | 检查项 | 通过标准 |
|---|---|---|
| 1 | 包体积 | 主包、每个分包、总包均低于平台限制,并记录具体大小 |
| 2 | 页面归属 | 启动页和 tabBar 页在主包;每个页面只声明一次 |
| 3 | 路由回归 | 首页、tab、分享、扫码、消息、支付回调路径均可打开 |
| 4 | 跨包依赖 | 无未声明的主包依赖、跨包同步 import、独立分包主包引用 |
| 5 | 预下载 | 每条 preloadRule 记录网络、触发页、包列表与合计体积 |
| 6 | 注入 | lazyCodeLoading 生效,无用 usingComponents 已删除 |
| 7 | 渲染 | 首页骨架在初始 data,initialRenderingCache 二次启动生效 |
| 8 | 降级 | 分包下载失败、请求失败、低版本基础库均有兜底 UI |
| 9 | 性能 | 同口径记录 baseline/treatment 的包大小、appLaunch、firstRender、内容可见 |
| 10 | 平台差异 | 微信、Taro/uni-app 生成产物均已核对,H5 或其他端不受影响 |
十三、落地路线图#
-
第 1 天:盘点
导出页面表、路由表、组件引用表、静态资源表和构建产物大小,确定主包 Top 10 文件。 -
第 2 天:第一轮拆分
移动低频业务域与专属资源,保留 tabBar/首页,更新路由常量,跑通核心路径。 -
第 3 天:注入与首屏
开启lazyCodeLoading,清理全局组件,为低频重组件加占位;首页骨架写入初始data,启用initialRenderingCache。 -
第 4 天:预下载策略
只为核心高转化分包配置预下载,核算同源 2M 限额,用 vConsole 验证触发。 -
第 5 天:测量与回归
固定设备、基础库、网络、版本和入口,对比包体积、appLaunch、firstRender、路由耗时、内容可见和错误率。 -
发布后
保留性能开关和回滚方案,观察线上分设备分场景数据;新增页面必须提交路由与包归属审查。
官方参考#
- 微信小程序分包加载
- 使用分包
- 独立分包
- 分包预下载
- 分包异步化
- 按需注入和用时注入
- 初始渲染缓存
- 小程序启动流程介绍
- wx.getPerformance
- PerformanceEntry
- 微信开发者工具 Audits
- Taro 微信小程序独立分包
- Taro 智能提取分包依赖
- uni-app pages.json 页面路由
- uni-app manifest.json 应用配置
结论#
分包解决的是启动下载单元,按需注入解决的是代码执行范围,初始渲染缓存解决的是二次可见时间,预下载解决的是后续路由断层,业务请求与骨架解决的是用户真正看到内容的时间。不要用单一指标宣称胜利:主包、总包、启动耗时、路由耗时、内容可见、失败率和流量必须放在同一张报表里。
对大多数项目,最稳妥的顺序是:先建立测量口径,再拆低频业务和资源,随后开启 lazyCodeLoading 并清理组件声明,最后精细化 preloadRule 和独立分包。每一步都保留回滚能力,用正式版真机数据验证,才能让 2026 年的小程序启动优化可持续。