同一版只能改一处时,先回答一个问题:这次冷启动的时间,花在代码包下载上,还是花在首屏渲染上。这道题不定下来,后面所有优化都是猜。一次冷启动能拆成五段,分叉只有三个台阶——下载占启动耗时的比例、首屏那几秒的去向、留存是否真被启动拖住。三个台阶走完落到四个动作上,九十天的排期才有依据。
取点用 wx.getPerformance()。它给五类 entryType:navigation 下的 appLaunch 是启动耗时,render 下有 firstRender、firstContentfulPaint、largestContentfulPaint,script 下的 evaluateScript 是注入脚本耗时,loadPackage 下的 downloadPackage 是代码包下载耗时(基础库 2.24.0 起提供),resource 下是视图层资源加载(wx.getPerformance)。五段相加才是完整的一笔账;只报 appLaunch 一个总数,分叉无从下手。版本门槛也要一起记:老基础库上这段取不到,占比结论得重算,不能跨版本对比。
第一级分叉:downloadPackage 除以 appLaunch。不到两成,包体不是这一段的主要成本,先去首屏;超过两成,清主包和拆包才可能赚回比真机波动更大的时间。两成是我给的工程阈值,不是微信口径。它成立的条件只有一个:同一台真机上的启动抖动,小于拆包能省下的时间。低端 Android 上这条条件常常落空,所以我把线画得保守;你该拿自己的机型分布重标一遍,不必照抄。
包体也不是一条能一路往下压的曲线,它更像一道门槛。整个小程序所有分包合计不超过 30M,服务商代开发的不超过 20M,单个分包或主包不超过 2M(分包加载)。离 2M 很远时,拆包省下的毫秒数常小于机型波动;贴着 2M 时,问题已经从「慢」变成「下一版功能发不出去」。所以第一级分叉里真正先做的动作往往不是拆,而是清:把首屏不用的图片和字体、整包引入的第三方 UI 库、重复的图标从主包挪走。主包里究竟装了什么,看开发者工具的代码依赖分析,别靠目录名猜。
下面这张表里两类来源不能混用。标「微信文档原文」的是写死的限制,越线就是错误,得当成缺陷修;标「工程经验」的是我按真机和排期给的线,越线只表示该先做哪件事,不是达标线。
| 分叉 | 指标 | 阈值 | 来源 | 越线先做什么 |
|---|---|---|---|---|
| 包占比 | downloadPackage / appLaunch | 两成 | 工程经验 | 先修首屏取数与 setData |
| 包体 | 单个分包或主包 | 2M | 微信文档原文 | 清主包,不加新分包 |
| 包体 | 所有分包合计 | 30M | 微信文档原文 | 停新增分包,合并旧包 |
| 包体 | 服务商代开发合计 | 20M | 微信文档原文 | 按 20M 重定预算 |
| 包体 | 主包预警线 | 1.6M | 工程经验 | 下轮迭代前先清 |
| 首屏 | 首屏时间 | 5 秒 | 微信文档原文 | 查接口层数与瀑布 |
| 渲染 | 渲染时间 | 500ms | 微信文档原文 | 减节点,分段 setData |
| 渲染 | 单次 setData(JSON.stringify 后) | 256KB | 微信文档原文 | 只传模板用到的字段 |
| 渲染 | 每秒 setData 次数 | 20 次 | 微信文档原文 | 合批,避开渲染期 |
第二级分叉在第一级通过之后才走:占比不高、首屏仍慢,时间就去向问题。先比 firstRender 与首屏接口耗时谁占大头。接口串行、接口回来才 setData、一次把整页数据推给视图层,这三种情况改样式没有用。改法顺着数据链路往下推:接口先合,非首屏的数据等首屏画完再取,setData 里只留模板真正用到的字段,长列表先出一屏,依赖渲染的逻辑挂到 onReady 之后。
注入侧还有一个开关。在 app.json 配 "lazyCodeLoading": "requiredComponents"(基础库 2.11.1 起),启动时只注入当前页面所需的页面与自定义组件代码,未访问页面不加载(按需注入和用时注入)。两个前提常被漏掉:页面 JSON 声明的组件、app.json 里 usingComponents 全局声明的组件,都会照常注入;全局声明里使用率低的组件越多,这个开关的效果越弱。插件包和扩展库暂不支持按需注入,要异步引入得先放进分包。副作用有一处最容易被读反:打开之后 evaluateScript 只算公共部分,页面与组件自己的注入时间被算进 firstRender,脚本耗时降低而渲染时间提高,文档写明属于正常现象,裁决指标是 appLaunch,不是 evaluateScript。
第三级分叉用来推翻前两级:把用户按首屏 P90 分成快慢两组,比次日留存。两组拉不开差距,说明启动不是留存的瓶颈,前面省下的几百毫秒换不来留存,该去看首屏之后的内容与流程;差距明显,再回到第一、第二级继续压。这条不给公布阈值,因为留存受品类和入口影响太大,只能用你自己的分组对比,套外部数字只会得出错的优先级。
拆包经常不动首屏,原因在打包规则。主包放默认启动页面、TabBar 页面,以及所有分包共用的公共资源和 JS,subPackages 配置路径之外的目录也打进主包。第一屏页面、它依赖的公共逻辑和图片因此必须留在主包,能拆出去的多是「我的」「订单详情」这类二级页,省下的是二级页首次进入时间,不是首屏时间,报告里把这两件事分开写。
入口以扫码和分享卡片为主时,独立分包比继续拆主包有效。独立分包脱离主包单独运行,从它的页面进入不下载主包,主包推迟到进入普通页面才下载;代价是不能引用主包的 JS、样式和组件,全局样式不生效,只适合不依赖登录态的那几个落地页。分包预下载走 app.json 的 preloadRule,network 默认 wifi、可设 all,同一分包内的页面共享预下载额度,超出在工具打包时校验失败(分包预下载)。预下载是额度分配,不是全勾上:只把首屏之后最可能进入的那个分包排进队列;设 all 会在移动网络下也下载,体验更好,代价是用户流量。
验证在真机上做,不在工具里做。清掉本地代码包,冷启动重复 20 次取 P50 与 P90,iOS 和一台低端 Android 各跑一遍。20 次和 P90 都是工程经验,不是文档要求,理由是用户感知的是长尾不是均值。要看长期趋势,用 wx.reportPerformance 把自定义点位上报,按机型、网络、版本传维度,接口要求先在小程序管理后台配置指标 id。采集口径一起记:基础库版本、机型、网络类型、是否首次进入。同一台机器换基础库版本,appLaunch 就不可比,这是月度对比最常见的一处失真。
走到叶子上,四个动作按分叉结果对号入座:
- downloadPackage 占比低于两成,先改首屏取数与 setData,包体排到下一版;
- 占比过两成、主包已过 1.6M,先清主包里首屏不用的图片和全局组件,再谈分包;
- 首屏过 5 秒且 firstRender 占大头,先减首屏节点与 setData 字节,别动分包;
- 留存快慢两组拉不开,先停启动优化,去查首屏之后的内容与转化路径。
有两种情况不在这棵树上。用户以热启动为主时,小程序从后台切回来走 onShow,不重新下载代码包,包体不在这条链上,先修首屏取数更实际。downloadPackage 很小而首屏仍慢时,瓶颈在接口串行或渲染量,拆包只占排期。九十天这个节点还适合顺手把预算写进发布检查项:主包上限取 2M 减团队约定的缓冲,首屏目标不拿 5 秒当目标,5 秒是及格线;再加一条每版允许增加的体积,让每个需求自带体积成本。





