用 Cursor 优化 uniapp 列表页:虚拟列表与按需更新实践
我为什么开始用 AI 写小程序
先说说我的处境。我在一个小团队做前端,主要用 uniapp + Vue 2 写小程序。之前我一直觉得 AI 写代码就是个玩具,顶多写写 demo 或者补注释,真要上生产还得靠人工调优。但上个月接了个社区团购小程序——商品列表、购物车、订单管理,还要实时显示库存,需求方要求 10 天上线,团队只有两个人。时间紧得我只能硬着头皮试 Cursor。
原本我打算让 AI 把基础页面和 API 对接写完,我自己再改性能问题。结果发现,AI 在我完全没想到的地方自动做了优化,甚至比我手写的代码更清楚小程序页面的性能瓶颈在哪里。
我写的列表页为什么卡?
先自曝一下我的老问题。以前写 uniapp 商品列表页,我习惯用 v-for 直接渲染全部数据,配合 onReachBottom 做分页加载。逻辑很简单:页面加载时请求第一页,滚到底部时请求下一页,新数据 concat 到原数组。列表项里每个商品都带图片、价格、标签、库存状态。
这种写法在小数据量(几十条)时没问题,但列表项超过 200 条就开始卡,尤其低端安卓机上滑动时掉帧明显。我之前的优化手段就是加个 v-if 控制图片懒加载,或者用 wx:if 代替 v-show,效果有限。
用 Cursor 时,我给的提示词很简单:“用 uniapp 写一个商品列表页,支持分页加载,列表项包含图片、价格和库存,注意性能。”它输出的代码让我有点意外,因为它自动做了几件我在写类似页面时根本没想到的事:
- 用 virtual-list 组件代替了 scroll-view + v-for。
- 对图片做了预加载和内存回收。
- 对库存状态变化做了按需更新,而不是整个列表重新渲染。
三个被 AI 提醒的优化细节
- 虚拟列表:不是所有数据都要渲染
AI 生成的代码直接用了 uniapp 的 uni-virtual-list 组件。这个组件我之前知道,但总觉得配置复杂,担心兼容性问题,一直没敢用。AI 却毫不犹豫地用了,而且配置得很干净。
核心思路是只渲染当前可见区域内的列表项,超出视口的 dom 节点被回收。比如一个 500 条数据的列表,实际 dom 节点可能只有 20 个。这直接解决了滑动卡顿的问题。
AI 生成的代码大致长这样(我做了简化):
javascript
{{ item.name }}
{{ item.price }}
而我之前手动写的是嵌套在 scroll-view 里的 v-for。两者性能差距在真机上非常明显:同样 300 条数据,用虚拟列表的页面滑动帧率稳定在 55fps 以上,而我的老写法在低端机上只有 20fps 左右。
坑点注意:uni-virtual-list 要求列表项高度固定,否则会计算错位。如果你的列表项高度不固定(比如有的商品描述文字多、有的少),需要提前做高度估算,或者用 item-key 配合动态高度。AI 在这里没处理动态高度,我是后来手动加了个 setTimeout 重新计算高度才解决的。
- 图片懒加载 + 内存回收:AI 自动加了“垃圾回收”
另一个让我惊讶的点是,AI 在图片加载上做了两层优化:
- 第一层:用 lazy-load 属性,让图片只在进入视口时才加载。
- 第二层:在 onUnload 生命周期里,手动清理图片缓存。
第二层优化我从来没想到过。小程序不像浏览器,它的内存管理很保守,大量图片加载后如果不释放,会导致页面越来越卡。AI 生成的代码里,这样写:
javascript
onUnload() {
this.goodsList.forEach(item => {
if (item.thumb) {
uni.downloadFile({ url: item.thumb, success: (res) => {
// 这里其实应该用 wx.cleanFileCache 之类的方法
// 但 AI 写的是手动释放内存,我改成了调用 SDK 方法
}})
}
})
}
虽然这个写法不够严谨(uni.downloadFile 不负责清理),但它提醒了我一个方向。后来我改成了调用 uni.getSavedFileList 和 uni.removeSavedFile 来清理临时文件。这个优化在用户频繁切换页面时效果很明显,页面回退后不会因为缓存堆积而卡顿。
我踩的坑:AI 生成的代码里,onUnload 中直接循环清理,但如果列表很大(比如 1000 条),这个操作本身就会卡住页面。我后来加了个 nextTick 和分批清理的逻辑,类似这样:
javascript
let batchSize = 50
for (let i = 0; i {
this.goodsList.slice(i, i + batchSize).forEach(...)
}, 0)
}
- 按需更新库存:避免整个列表重渲染
我们项目的库存是实时变化的,用户下单后,其他用户的页面上库存数字要立刻更新。以前我的做法是:收到 WebSocket 推送后,重新请求整个列表数据,然后覆盖 goodsList。这会导致整个列表重新渲染,即使只改了一个商品的库存。
AI 生成的代码里,它用了一个很巧妙的方案:维护一个 stockMap 对象,键是商品 ID,值是库存数量。列表渲染时,直接从 stockMap 读取库存,而不是从商品数据里取。更新时只更新 stockMap 中对应 ID 的值,不触发整个列表的 setData。
代码思路:
javascript
data() {
return {
goodsList: [],
stockMap: {} // { goodsId: stock }
}
},
methods: {
updateStock(goodsId, newStock) {
this.stockMap[goodsId] = newStock
// 只更新 stockMap,不重新设置 goodsList
this.$forceUpdate() // 或者用 Vue.set
}
}
虽然 $forceUpdate 仍然会触发一些组件更新,但比重新设置整个 goodsList 要轻量得多。而且 AI 在模板里用 stockMap[item.id] 而不是 item.stock,这一点我手动改代码时经常忽略。
代价:这个方案会增加内存开销,因为多维护了一个 stockMap 对象。但相比性能提升,这点内存完全可以接受。如果你的列表项超过 10000 条,可以考虑用 Map 代替普通对象,或者用 WeakMap 做自动回收。
AI 不是万能的,需要人工“兜底”
虽然 Cursor 帮我写了不少好代码,但我也发现了一些需要人工处理的问题:
-
虚拟列表的滚动事件兼容性:uni-virtual-list 在 iOS 上偶尔会出现滚动位置偏移,AI 没处理。我手动加了一个 @scroll 事件监听,用 scrollTop 做二次校准。
-
图片懒加载的占位图:AI 生成的代码没给 image 加 placeholder 属性,导致图片加载前显示空白,用户体验不好。我手动加了 @error 和 @load 事件来处理加载失败和成功。
-
库存更新的并发问题:如果 WebSocket 推送太快(比如秒杀场景),stockMap 的更新可能会覆盖。我加了一个队列机制,保证每次更新都是基于最新值。
-
代码可读性:AI 生成的代码虽然高效,但变量命名很随意(比如 item、temp、arr)。如果不重命名,团队协作时别人看不懂。我花了半小时把变量名改成有业务含义的。
总结:AI 帮我打开了性能优化的新视角
这次用 Cursor 写 uniapp 小程序的经历,让我意识到两件事:
- 第一,AI 在性能优化上的“直觉”比很多开发者都强,因为它训练的数据里包含了大量真实项目的性能优化模式,而这些模式我们平时很少主动学习。
- 第二,AI 不能替代开发者的判断力,但可以作为一个“性能顾问”,帮你指出那些你根本没想到的优化点。
如果你也在用 uniapp 做小程序,强烈建议试试让 AI 帮你生成列表页、详情页这类高频渲染的页面。但记得,AI 输出的代码要逐行审查,尤其是边界条件和内存管理。毕竟,最懂你业务逻辑的,还是你自己。
最后,如果你也在用 Cursor 或者 Copilot 写 uniapp,欢迎在评论区分享你遇到的坑和优化技巧。