性能问题的三个来源
小程序的性能问题,用户往往只说得出「卡」和「慢」两个词,但在工程上它们对应完全不同的环节。把它拆开看,来源基本落在三处:启动链路(从点开小程序到首屏可交互)、渲染链路(数据变化到界面更新)、交付体积(用户要为这次访问下载多少代码与资源)。三者互相牵制,只优化其中一环,体验提升很快就会触到天花板。
先建立度量再谈优化,是所有性能工作里性价比最高的一步。开发者工具的性能面板能拿到启动耗时、页面渲染耗时与内存占用,再配合基础库的性能接口把真实用户数据上报,你才有「改前改后到底差多少」的判断依据。没有基线的优化,多数时候只是把感觉当成了证据。
性能不是某个接口的开关,而是一条从点击到呈现的完整链路。哪一环最慢,就先修哪一环。
- 冷启动代码包下载、注入执行与首屏渲染共同决定首次打开速度。
- setData跨线程通信有成本,数据越少越小、越晚发送越好。
- 分包加载把主包压到限制以内,其余业务按需拉取。
- 持续监控用真实用户数据看分位数,而不是只看平均值。
启动性能:冷启动与首屏
冷启动指小程序进程尚未存在时的一次完整加载,它由三段耗时叠加而成:代码包下载、逻辑注入与执行、首屏渲染。热启动则复用已有进程,通常只做页面切换,快得多。用户抱怨的「打开慢」,九成说的是冷启动,所以度量要盯住这三段分别花了多久。
| 阶段 | 主要耗时来源 | 常见优化手段 |
|---|---|---|
| 代码包下载 | 包体积大、弱网环境 | 分包、去除无用资源、预下载 |
| 逻辑注入执行 | 同步初始化任务过多 | 延后非关键初始化、拆分同步逻辑 |
| 首屏渲染 | 首屏接口串行、依赖过多 | 骨架屏、本地兜底数据、请求并行与缓存 |
首屏优化有一条朴素但有效的原则:先渲染结构,再填充数据。进入页面时先把骨架与静态文案画出来,让用户马上看到反馈;接口返回后再替换为真实内容。同时把多个互不依赖的请求并行发出,别让它们排队等前一个的结果。
还有一点容易被忽略:全局的 app.js 里如果塞了太多同步逻辑,比如同步读取大量本地缓存、同步计算字典,它会挡住所有页面。把这些任务挪到首个页面渲染完成之后再跑,或者干脆改成异步,冷启动时间往往能立竿见影地下降。
渲染性能:setData 与长列表
小程序采用双线程模型:逻辑层跑在 JSCore,视图层负责渲染,两者之间靠 setData 传递数据。这意味着每次调用都要把数据序列化后跨线程传输,数据越大、频率越高,卡顿越明显。优化的核心思路只有一句:只传变化的部分,尽量少传、尽量晚传。
// 只更新变化的那一项,避免整表重渲染
const idx = 3;
this.setData({
["list[" + idx + "].done"]: true,
progress: 42
});
路径写法能让框架精确知道哪一项变了,从而只刷新对应的节点。相比之下,把整张列表重新赋值一次,视图层就要重建全部节点,代价差了好几倍。几条经验规则值得记住:
- 不要把与渲染无关的数据放进
data,需要保留就挂在页面实例的其他字段上。 - 滚动、拖拽这类高频事件里避免连续调用
setData,先节流或合并成一次。 - 长列表用分页或虚拟列表,控制同时在屏的节点数量。
- 图片给定固定宽高并开启懒加载,避免加载完成时页面跳动。
内存同样要盯。页面销毁时记得清掉定时器、事件监听与大数据引用,否则反复进入离开会让内存只涨不降,最终触发闪退。这类问题在真机上比在开发者工具里更容易复现。
包体积:分包与依赖瘦身
包体积直接影响下载时间,平台对它有硬性限制:主包通常要求不超过 2MB,整个小程序所有分包合计也有上限。当业务越做越大,把全部代码塞进主包迟早会撞线,这时就要靠分包把体积拆开。
{
"pages": ["pages/index/index", "pages/mine/mine"],
"subPackages": [
{
"root": "packageA",
"pages": ["pages/detail/detail", "pages/list/list"]
}
],
"preloadRule": {
"pages/index/index": { "network": "all", "packages": ["packageA"] }
}
}
分包的基本约定是:主包放启动页面与公共代码,其余业务按模块拆到分包里。用户访问不到的分包不会被下载,首屏体积自然变小。配置里的 preloadRule 用来做分包预下载,用户还在看首页时,让常用的分包在后台悄悄下好,跳转时就不会白屏等待。
- 公共组件与工具库放主包,避免每个分包各存一份造成重复。
- 把低频功能,如设置、帮助、历史记录,拆到独立分包。
- 清理未引用的图片与依赖,体积往往能一次性瘦下来不少。
资源与网络优化
体积不只来自代码,图片、字体与静态资源常常才是大头。一张未压缩的高清图动辄几百 KB,列表里放上十几张,流量和渲染压力都会陡然上升。
| 资源类型 | 常见问题 | 优化手段 |
|---|---|---|
| 图片 | 尺寸过大、格式陈旧 | 按显示尺寸裁剪、改用 WebP、接入 CDN 压缩 |
| 网络请求 | 串行、缺少缓存 | 请求并行、合并、设置合理缓存策略 |
| 本地存储 | 同步接口阻塞主线程 | 优先异步接口,控制单次读写数据量 |
网络这一层,优先做两件事:减少请求数和用上缓存。能合并的接口合并,能缓存的列表页结果就别每次重新拉。首屏只请求必须的数据,剩余内容滚动到可见区域再加载,用户感知到的速度会明显不同。
上线后的性能监控
本地跑得再快,也不代表真实用户的环境一样。开发者的电脑通常网络好、机型新,而用户的设备可能老旧、信号微弱。性能优化的最后一步,是把度量搬到线上,用真实数据回答「到底有没有变快」。
- 采集:在关键节点埋点,记录启动耗时、首屏耗时与页面切换耗时。
- 统计:关注 P90、P95 分位,而不是平均值,平均值会掩盖掉一批真实卡顿的用户。
- 分维度:按机型、系统版本与网络类型分组对比,才能定位是通用问题还是特定环境问题。
- 对齐版本:每次发版前后都留观察窗口,确认改动带来的收益,而不是凭感觉下结论。
把监控做成常态,性能就从一个阶段性的专项变成了可持续的指标。当曲线出现异常上扬,你能第一时间发现,而不是等用户来投诉。
可落地的优化清单
把前面的内容收敛成一份可以直接执行的清单,按投入产出比排序。
// 首屏耗时埋点:进入页面记一次,首帧渲染后再记一次
Page({
onLoad() {
this.t0 = Date.now();
},
onReady() {
const cost = Date.now() - this.t0;
wx.reportEvent("first_screen", { cost: cost });
}
});
埋点只是第一步,真正产生收益的是按顺序做这几件事:
- 先量后改:把启动、首屏、页面切换三个耗时指标先采集起来。
- 砍首屏:非关键初始化延后,首屏接口并行并加缓存。
- 收 setData:只传变化数据,合并高频更新。
- 拆分包:主包压到限制以内,低频功能移出主包。
- 压资源:图片按需裁剪与压缩,能懒加载就懒加载。
性能优化没有终点,但有一条清晰的顺序:先让它可度量,再让它更快,最后让它一直快下去。