▶_MiniApp Toolkit
· 约 25 分钟微信小程序/Taro/uni-app

小程序分包加载与启动性能优化实战指南 2026

分包加载subpackages启动性能优化分包预下载lazyCodeLoading独立分包Tarouni-app小程序性能优化
目录

小程序分包加载与启动性能优化实战指南 2026#

分包不是「把目录挪走」这么简单。它同时改变下载单元、注入范围、路由路径、资源归属和跨包依赖;如果只做机械拆分,常见结果是:主包依然接近 2M,进入业务分包时额外等待下载,甚至因为跨包引用而在真机白屏。本文把分包加载和启动性能放在同一条链路里处理:先明确启动阶段,再决定拆什么、预下载什么、注入什么、缓存什么,最后用同口径数据验证效果。

本文以微信小程序官方能力为主,并给出原生微信、Taro、uni-app 三套配置。所有平台版本、大小上限和基础库要求均来自文末官方文档;性能收益没有通用固定值,请按第 10 节的方法用自己的正式版、真机和真实用户路径量化。

一、先理解启动链路:优化点在哪里#

微信官方将小程序启动过程划分为运行环境准备、小程序信息准备、代码包准备、逻辑层注入、视图层注入、首页渲染,以及异步数据到达后的首屏内容展示。两个关键前提必须记住:

  1. 各阶段会尽可能并行,不能把每个阶段耗时简单相加来计算总启动时间。
  2. 框架层面以首页 Page.onReady 触发作为启动过程完成。但如果首页主体内容依赖 wx.requestonReady 后仍可能出现业务意义上的白屏或骨架屏。
启动阶段主要内容开发者可控度对应优化
运行环境准备进程、WebView、JS 引擎、基础库等基本由微信客户端控制不宣称自己优化了该阶段;用真机分设备统计
小程序信息准备版本、配置、权限等元信息缓存/更新间接影响合理规划发版,避免把每次实验都推给全量用户
代码包准备下载、校验启动所需代码包主包瘦身、分包、独立分包、预下载
逻辑层注入配置、页面、组件、业务 JS 注入分包、lazyCodeLoading、移除无用依赖
视图层注入WXML/WXSS 编译产物注入降低首页结构复杂度、减少组件声明
首页渲染初始 data 渲染并触发 onReady初始 data、初始渲染缓存、组件用时注入
首屏内容展示异步请求返回并 setData骨架屏、请求预发起、失败兜底、关键路径拆分

因此,一份可执行的启动优化地图应该是:

  • 下载更少:主包只保留启动页、tabBar 页和必要公共代码。
  • 提前下载:对高转化后续分包使用 preloadRule,但控制网络和体积。
  • 注入更少:启用 lazyCodeLoading: "requiredComponents",清理无用 usingComponents
  • 更早可见:对稳定首屏使用 initialRenderingCache 与骨架状态。
  • 更早有内容:关键请求与页面初始化并行,异步内容有加载/失败状态。
  • 可验证:包体积、appLaunchfirstRender、路由耗时、业务内容时间分开记录。

反例:只把主包从 1.9M 拆到 1.2M,不处理全局组件和首页请求,用户看到有意义内容的时间可能没有明显变化。包体积是输入指标,不是唯一结果指标。

二、分包基础:主包、普通分包与官方边界#

2.1 打包单元#

微信小程序使用分包后,一定会包含一个主包。官方对主包的定位是:

  • 默认启动页面;
  • tabBar 页面;
  • 所有分包都会用到的公共资源与 JS 脚本。

普通分包在用户进入其中页面时才下载。也就是说,分包优化的是「启动时需要下载的代码」,不是把所有代码都变成免下载。若某个分包是首页必经路径,只拆分而不预下载,启动体验未必变好。

官方大小限制:

限制项上限
整个小程序所有分包合计30M
服务商代开发的小程序合计20M
单个主包或单个分包2M

这些是平台配置的硬边界。项目里更应该建立更严格的内部预算,例如「主包目标 ≤ X M」「启动必经分包 ≤ Y M」,具体阈值由业务基线和低端设备数据决定,而不是贴着 2M 上限发布。

分包能力要求微信客户端 6.6.0、基础库 1.7.3 及以上。独立分包、预下载、按需注入等能力有更高要求,见后续各节。

2.2 原生微信完整配置#

下面示例展示一个电商/交易类小程序的常见划分:

code
// 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 可以使用 rootname
  • independent: true 声明独立分包。
  • __APP__ 表示预下载主包,常用于从独立分包回流主应用。
  • preloadRule.network 不配置时默认仅 Wi-Fi;all 表示不限网络。
  • lazyCodeLoading 只写 "requiredComponents",不要凭记忆写其他值。

2.3 拆分策略#

建议按「启动必要性 + 业务域 + 共享关系」三步拆:

  1. 启动必要性:首页、tabBar 页、登录兜底、全局错误页、极小公共 shell 留主包。
  2. 业务域:交易、活动、内容编辑、设置、低频工具按域拆分,避免一个 pages-sub 变成新的巨型包。
  3. 共享关系:只有主包确实使用的依赖放主包;仅某个分包使用的大依赖移动到该分包;多个分包共同依赖的模块要比较「放主包」「复制到各包」「分包异步化」的体积与加载成本。

目录示意:

code
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 页面必须在主包内。

如果确实需要跨分包复用,优先考虑以下顺序:

  1. 下沉主包:模块很小且主包也使用,可以放主包。
  2. 复制轻量模块:极小纯函数复制到各包,换取加载独立性,但要接受维护成本。
  3. 服务端聚合:跨业务状态由后端接口统一,避免客户端全局时序假设。
  4. 分包异步化:基础库 2.11.2+ 支持异步引用其他分包 JS/组件/插件,适合低频大依赖。

跨分包 JS 示例:

code
// 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

code
// 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

code
// 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 规则设计#

code
{
  "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 中配置:

code
{
  "lazyCodeLoading": "requiredComponents"
}

基础库 2.11.1+ 支持。开启后,小程序只注入当前访问页面所需的页面和自定义组件代码;未访问页面、当前页面未声明的组件不会加载和初始化。

常见误解是「配置一行就结束」。实际上,页面 JSON 中声明的所有组件和 app.json 全局 usingComponents 都会被视为依赖。因此要同步做:

  1. 删除页面中已不再使用的 usingComponents
  2. 避免全局声明低使用率组件;
  3. 将组件声明移动到真正使用它的页面;
  4. 检查模板动态渲染和抽象节点是否仍能解析;
  5. 回归测试所有入口,尤其是分享进入、扫码进入、订阅消息进入。

插件包和扩展库目前不支持按需注入。若插件体积大,可考虑放入分包并通过分包异步化异步引入。

6.2 用时注入与占位组件#

基础库 2.11.2+ 支持在开启按需注入后使用「用时注入」:为自定义组件配置占位组件,组件第一次渲染前不注入;第一次渲染时先展示占位,再注入并替换。

code
{
  "usingComponents": {
    "editor": "../heavy-editor/index",
    "editor-skeleton": "../editor-skeleton/index"
  },
  "componentPlaceholder": {
    "editor": "editor-skeleton"
  }
}

适用场景:

  • 富文本编辑器;
  • 图表和大屏可视化;
  • 地图复杂覆盖物;
  • 客服/直播/上传等低频重组件。

不适用场景:

  • 首屏关键按钮;
  • 需要立即可交互的表单控件;
  • 占位组件高度不稳定导致明显布局抖动的区域。

6.3 initialRenderingCache:第二次进入更早可见#

页面配置:

code
{
  "initialRenderingCache": "static"
}

基础库 2.11.1+ 支持。它会在页面第一次打开后记录「页面初始 data」的渲染结果,页面第二次打开时先直接展示缓存,不等逻辑层初始化完成。

它适合:

  • 导航壳;
  • 稳定骨架;
  • 固定标题与背景;
  • 首屏中不依赖异步请求的结构。

必须注意:

  • 不包含后续 setData 的结果
  • 展示缓存时页面暂时不能响应事件,等逻辑层初始化完成后才可以;
  • 缓存可能因小程序更新、基础库更新、存储回收等原因被清除;
  • 缓存阶段复杂组件有限制,官方支持的内置组件包括 viewtextbuttonimagescroll-viewrich-text;自定义组件可出现,但其中的内置组件也要遵循限制。

正确写法是把首屏骨架状态放进初始 data

code
// pages/home/index.js
Page({
  data: {
    shellReady: true,
    listStatus: 'loading', // loading | success | empty | error
  },
});
code
<!-- 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 被多个页面同步引用;
  • 图片和字体被放在根目录,被工具归入主包。

原生项目的检查顺序:

  1. 导出 app.json 与所有页面 JSON 的 usingComponents,统计实际引用;
  2. 用构建产物或包体积分析确认每个大文件归属;
  3. 只被一个分包使用的组件移动到该分包;
  4. 多包共用且主包也使用的组件留在主包,但做按需声明;
  5. 跨分包低频重组件用占位组件和分包异步化;
  6. 大图上传 CDN,包内只保留必要占位图。

八、Taro 完整配置示例#

Taro 的分包配置最终会编译到微信小程序 app.json。下面是一个 React/Taro 项目示例:

code
// 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',
  },
};

跳转路径必须带分包根目录:

code
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+ 提供智能提取分包依赖,可把主包未依赖、分包独占的模块提取到分包,减少主包体积:

code
// 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.jsonmp-weixin 节点配置微信平台差异项:

code
// 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"
  }
}
code
// 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

页面跳转:

code
uni.navigateTo({
  url: '/package-trade/pages/order-detail/index?id=1001',
});

构建后应检查 dist/dev/mp-weixin/app.jsondist/build/mp-weixin/app.json

  • subPackages / subpackages 是否生成;
  • preloadRule 是否保留;
  • lazyCodeLoading 是否在生成的 app.json 顶层;
  • 独立分包是否意外引用了主包公共样式或组件;
  • 静态资源是否被编译进主包。

十、性能测量:先定口径,再看数据#

没有基线的性能优化很容易变成「感觉更快」。推荐把指标分为输入指标、框架指标和业务指标。

10.1 建立基线#

每次对比至少固定以下条件:

条件要求
版本尽量使用正式版;开发版/体验版链路更慢,不能直接外推
基础库同一基础库版本
设备至少一台低端 Android 与一台 iOS;分别记录
网络Wi-Fi 与弱网分开;记录网络类型
缓存状态首次冷启动、第二次启动、版本更新后分开
入口首页、分享落地页、扫码路径、消息路径分开
样本每个口径保留多样本,看中位数、P75、P90,不只看平均值

记录表建议:

指标来源口径
主包大小构建产物/上传结果压缩后包体积
各分包大小构建产物/上传结果压缩后包体积
预下载合计preloadRule + 构建产物同源包 2M 限额内合计
appLaunchwx.getPerformance navigation entry启动耗时
routewx.getPerformance navigation entry页面路由耗时
firstRenderwx.getPerformance render entry首次渲染
首页 onReady页面生命周期打点框架启动完成
内容可见首屏关键请求回调/骨架替换业务可感知时间
交互可用关键按钮可点击打点用户可行动时间

10.2 wx.getPerformance 采集示例#

code
// 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.jsonLaunch 尽早初始化,并把版本、机型、系统、基础库、网络、入口场景一起上报。注意:

  • 不要只上报 duration,缺少设备与场景的数据无法归因;
  • 不要把一次本地测试当作结论;
  • 不要把 onReady 等同于用户看到内容;
  • 不要比较不同基础库、不同网络、不同缓存状态下的数据。

10.3 开发者工具 Audits 与体验评分#

微信开发者工具调试器的 Audits/体验评分适合做发布前静态与交互检查:

  1. 使用接近正式包的构建产物;
  2. 选择目标基础库;
  3. 清理无关调试面板与日志;
  4. 执行真实路径:冷启动首页、点击核心分包、返回、弱网、失败重试;
  5. 查看 Performance / Experience / Best Practices 分类;
  6. 把每条问题映射到责任人与修复版本,而不是只记录总分。

体验评分适合发现「不符合平台最佳实践」的问题;启动耗时归因仍要结合 wx.getPerformance、真机和线上采样。不要把一次 Audits 分数当作性能回归的唯一门禁。

10.4 量化优化效果#

建议报告使用同口径相对变化,并保留样本量:

code
实验 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 内路径
修复:集中维护路由常量,禁止在页面里散落字符串拼接。

code
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渲染首页骨架在初始 datainitialRenderingCache 二次启动生效
8降级分包下载失败、请求失败、低版本基础库均有兜底 UI
9性能同口径记录 baseline/treatment 的包大小、appLaunchfirstRender、内容可见
10平台差异微信、Taro/uni-app 生成产物均已核对,H5 或其他端不受影响

十三、落地路线图#

  1. 第 1 天:盘点
    导出页面表、路由表、组件引用表、静态资源表和构建产物大小,确定主包 Top 10 文件。

  2. 第 2 天:第一轮拆分
    移动低频业务域与专属资源,保留 tabBar/首页,更新路由常量,跑通核心路径。

  3. 第 3 天:注入与首屏
    开启 lazyCodeLoading,清理全局组件,为低频重组件加占位;首页骨架写入初始 data,启用 initialRenderingCache

  4. 第 4 天:预下载策略
    只为核心高转化分包配置预下载,核算同源 2M 限额,用 vConsole 验证触发。

  5. 第 5 天:测量与回归
    固定设备、基础库、网络、版本和入口,对比包体积、appLaunchfirstRender、路由耗时、内容可见和错误率。

  6. 发布后
    保留性能开关和回滚方案,观察线上分设备分场景数据;新增页面必须提交路由与包归属审查。

官方参考#

结论#

分包解决的是启动下载单元,按需注入解决的是代码执行范围,初始渲染缓存解决的是二次可见时间,预下载解决的是后续路由断层,业务请求与骨架解决的是用户真正看到内容的时间。不要用单一指标宣称胜利:主包、总包、启动耗时、路由耗时、内容可见、失败率和流量必须放在同一张报表里。

对大多数项目,最稳妥的顺序是:先建立测量口径,再拆低频业务和资源,随后开启 lazyCodeLoading 并清理组件声明,最后精细化 preloadRule 和独立分包。每一步都保留回滚能力,用正式版真机数据验证,才能让 2026 年的小程序启动优化可持续。