上周把手上一个社区团购小程序的商品列表页重做了一遍。这个页面从上线起就有点慢,用户滑到第三屏开始掉帧,运营那边也反馈说加载时白屏时间长。我本来以为是小问题,结果一查数据,这个页面的跳出率比首页高了快一倍。这次优化前后折腾了三天,记录一下过程,给遇到类似情况的朋友参考。

先别急着改代码,先量一下
我的习惯是动手前先拿数据。微信开发者工具的性能面板和真机调试都用上了,重点看两个东西:一是首屏渲染耗时,二是滚动时的 setData 频率。结果不太好看:
- 首屏从 onLoad 到列表可见,平均 1.8 秒,低端机超过 3 秒
- 一次滚动触底加载 20 条数据,setData 传了整个数组
- 列表项里有个倒计时组件,每个 item 各自跑一个 setInterval
问题基本清楚了:数据量不大,但传递方式太粗暴,加上定时器滥用,逻辑层和渲染层之间的通信被拖垮了。
第一刀:setData 只传增量
原来分页加载的写法是这样的,每次把整个列表重新 set 一遍:
// 优化前
onReachBottom() {
this.page++
fetchList(this.page).then(res => {
this.setData({
list: this.data.list.concat(res.data)
})
})
}数据到 100 条以后,每次 setData 都在传一个几百 KB 的对象,序列化和跨线程传输的开销很吓人。改成用路径更新,只传新增的那一段:
// 优化后
onReachBottom() {
this.page++
fetchList(this.page).then(res => {
const start = this.data.list.length
const patch = {}
res.data.forEach((item, i) => {
patch[`list[${start + i}]`] = item
})
this.setData(patch)
})
}就这一处改动,滚动加载的卡顿感基本消失了。原理不复杂,setData 的数据量直接决定通信耗时,能少传就少传。
第二刀:干掉列表里的定时器
每个商品卡片有个“距结束还剩 xx:xx”的倒计时。之前的实现是每个组件在 attached 里起一个 setInterval,一屏十几个组件就是十几个定时器,而且每秒钟每个定时器都触发一次 setData。
改成页面级统一维护一个定时器,每秒只更新一次,并且只更新变化了的那几条:
// 页面里统一 tick
startTick() {
this.timer = setInterval(() => {
const now = Date.now()
const patch = {}
this.data.list.forEach((item, i) => {
if (item.endTime > now) {
patch[`list[${i}].countdown`] = formatLeft(item.endTime - now)
}
})
if (Object.keys(patch).length) this.setData(patch)
}, 1000)
}组件只负责展示,不再自己管时间。页面 onHide 和 onUnload 里记得 clearInterval,这个坑我踩过,切后台没清定时器,回来以后定时器叠了好几层。
第三刀:图片和骨架屏
列表图之前直接用的原图,一张 200KB 起步。后来让后端在 CDN 上开了缩放参数,列表里统一取 300px 宽的缩略图,配合 lazy-load,首屏图片流量降了七成多。
白屏的问题用骨架屏解决。不是上第三方库,就是页面初始 data 里放一个长度为 6 的占位数组,渲染灰色块,真实数据回来再替换。代码很少,但体感提升明显,用户至少知道页面在加载,不会以为卡死了。
改完后的数据
- 首屏可见时间从 1.8 秒降到 0.9 秒左右,低端机 1.5 秒内
- 滚动掉帧基本没有了,开发者工具里 FPS 稳定在 50 以上
- 页面跳出率降了大概 20%,这个有运营数据佐证
说点题外话。这次优化没用什么高深技术,全是基础操作,但效果比我想的明显。我见过不少项目一上来就想着上分包、上预加载,结果连 setData 都在传全量数据。性能优化这事儿,先量再改,别凭感觉。另外,真机测试一定要用中低端安卓机,开发者工具和 iPhone 上的表现会骗人。
这个项目后面还打算把列表虚拟化做一下,长列表场景下 DOM 节点太多还是会有压力。有进展再写一篇。

评论 (0)
还没有评论,快来抢沙发吧~