你有没有遇到过这种情况:在一个电商小程序里浏览商品列表,滑到三四十条的时候,手指开始感觉不跟手,再往下滑手机开始发烫,甚至小程序直接闪退。

这不是手机的问题,是列表渲染方式的问题。今天聊聊当数据量变大时,小程序列表为什么会卡,以及如何解决。

问题的根源:setData和渲染瓶颈

小程序中展示列表最直接的方式是这样的:

javascript
Page({
  data: {
    list: []  // 假设这里有1000条数据
  },
  onLoad() {
    wx.request({
      success: (res) => {
        this.setData({ list: res.data })  // 一次性设置1000条
      }
    })
  }
})

对应的WXML:

html
 
<view wx:for="{{list}}" wx:key="index">
  <view class="item">{{item.title}}</view>
</view>

这段代码看似简单,实际上存在两个性能问题。

第一个问题:setData传输的数据量过大。小程序逻辑层和渲染层是分开的两个线程,每次setData都会把数据通过序列化传输给渲染层。1000条完整数据可能达到几百KB甚至几MB,传输时间很长。

第二个问题:渲染层同时创建大量DOM节点。1000个列表项意味着1000个节点,每个节点都有布局、样式、事件绑定。浏览器或者小程序的渲染引擎处理这么多节点,内存占用和重绘成本都很高。

优化方向一:分页加载,减少单次数据量

最简单有效的优化是不要一次加载全部数据。

实现方式:

javascript
Page({
  data: {
    list: [],
    page: 1,
    hasMore: true
  },
  onLoad() {
    this.loadMore()
  },
  loadMore() {
    if (!this.data.hasMore) return
    
    wx.request({
      url: 'https://api.example.com/list',
      data: { page: this.data.page, pageSize: 20 },
      success: (res) => {
        const newList = this.data.list.concat(res.data.list)
        this.setData({
          list: newList,
          page: this.data.page + 1,
          hasMore: res.data.hasMore
        })
      }
    })
  },
  onReachBottom() {
    this.loadMore()  // 滚动到底部时加载下一页
  }
})

用户先看到前20条,滑到底部自动加载下一批。首屏渲染速度明显提升,单次setData的数据量也控制住了。

但分页加载并没有解决另一个问题:当用户滑到第10页时,页面中仍然存在200个节点。这时候滑动还是会逐渐变卡。

优化方向二:使用虚拟列表

虚拟列表的核心思路是:只渲染屏幕可见区域的那几条数据,以及上下各少量缓冲项。用户滑动时,动态替换可见区域的数据。

假设屏幕高度可容纳8个列表项,虚拟列表实际只渲染12个(可见8个加上下各2个缓冲)。用户滚动时,计算当前滚动位置对应的数据索引,更新这12个项的数据。

实现方式有两种。

第一种是使用官方提供的recycle-view组件。微信小程序从基础库2.8.1开始提供了长列表组件。

在页面的JSON中声明:

json
{
  "usingComponents": {
    "recycle-view": "/miniprogram_npm/miniprogram-recycle-view/recycle-view",
    "recycle-item": "/miniprogram_npm/miniprogram-recycle-view/recycle-item"
  }
}

WXML中使用:

html
 
<recycle-view batch="{{batchSetRecycleData}}" bindscrolltolower="loadMore">
  <recycle-item wx:for="{{recycleList}}" wx:key="id">
    <view class="item">{{item.title}}</view>
  </recycle-item>
</recycle-view>

recycle-view会自动管理节点创建和回收,开发者只需要提供数据即可。但需要注意这个组件有一定的学习成本,而且自定义能力有限。

第二种是自己实现虚拟列表。计算逻辑如下:

javascript
// 简化版虚拟列表逻辑
const ITEM_HEIGHT = 100  // 每个列表项固定高度(px)
const BUFFER_COUNT = 4   // 缓冲数量
let screenHeight = 0      // 屏幕高度
let startIndex = 0        // 当前显示的起始索引

function onScroll(scrollTop) {
  // 计算当前应该显示从哪条开始
  startIndex = Math.floor(scrollTop / ITEM_HEIGHT)
  startIndex = Math.max(0, startIndex - BUFFER_COUNT)
  
  const visibleCount = Math.ceil(screenHeight / ITEM_HEIGHT) + 2 * BUFFER_COUNT
  const visibleData = originalList.slice(startIndex, startIndex + visibleCount)
  
  this.setData({
    visibleList: visibleData,
    startIndex: startIndex,
    topPadding: startIndex * ITEM_HEIGHT  // 用空的占位撑开滚动区域
  })
}

这种方式效果很好,但需要列表项高度固定。如果列表项高度不固定,计算会复杂很多。

优化方向三:图片懒加载

长列表里的图片是另一个性能杀手。即使用户没滑到图片位置,图片资源也会被请求和渲染。

解决方案是使用image组件的lazy-load属性:

html
 
<image src="{{item.imageUrl}}" lazy-load mode="aspectFill"></image>

设置lazy-load后,图片只在即将进入可视区域时才开始加载。这能大幅减少网络请求和内存占用。

对于更精细的控制,可以自己实现懒加载:监听滚动位置,只对当前可视区域前后一定范围内的图片设置真实的src,其余图片用占位图。

优化方向四:使用scroll-view的增强特性

微信小程序提供了增强版的scroll-view组件,开启后会使用自定义模式处理滚动,性能更好。

配置方式:

html
 
<scroll-view 
  scroll-y 
  enhanced 
  show-scrollbar="{{false}}" 
  fast-deceleration="{{true}}"
  bindscrolltolower="loadMore">
  <!-- 列表内容 -->
</scroll-view>

其中enhanced属性开启增强模式,fast-deceleration让滚动更接近iOS原生体验。配合分页加载,很多中小型列表场景就不需要上虚拟列表了。

最佳实践组合

根据实际的列表规模选择合适的方案:

列表少于50条:分页加载就够了,不需要额外优化。首屏加载20条,滑动加载下一批。

列表在50到200条:分页加载加上图片懒加载,开启scroll-view的enhanced模式。这种组合下体验已经相当流畅。

列表超过200条:需要用虚拟列表。优先尝试官方的recycle-view组件,不能满足时再自己实现。

列表包含复杂子组件:如果每个列表项内部还有自己的图片轮播、视频播放、表单输入等复杂组件,虚拟列表的效果会打折扣。这种情况下应该考虑从根本上减少单页数据量,优化交互设计。比如分页控制在每页10条,引导用户按分类筛选而不是无限滚动。

一个容易被忽视的点:wx:key的作用

无论用哪种方案,wx:key都要正确设置。

html
 
<!-- 错误:使用index作为key,数据顺序变化时会有问题 -->
<view wx:for="{{list}}" wx:key="index">

<!-- 正确:使用数据中唯一的id -->
<view wx:for="{{list}}" wx:key="id">

正确的wx:key能帮助小程序框架高效地重用和重排现有节点,避免不必要的重新渲染。

测量与验证

优化的效果需要用数据说话。在开发者工具中打开调试器的Performance面板,录制滚动操作,查看帧率。真机上可以使用性能面板的“Trace”功能导出详细数据。

一个简单的自我验证方法:在优化前后分别滚动到列表的第100条,感受手指离开屏幕后页面是否还继续滑动一段距离。卡顿的列表滚动生硬,优化的列表滚动有惯性。

电话咨询
QQ咨询
在线咨询
服务投诉