取件伙伴性能提升------长列表
在移动应用开发中,List是最常见也是最容易出现性能瓶颈的场景之一。在 取件伙伴 项目中,取件列表页面需要展示可能多达数百条的包裹信息。如果不进行优化,随着数据量的增长,应用会出现滑动掉帧、内存占用过高甚至崩溃的问题,特别是最近我增加了在深色模式下的雪花效果,列表更是卡的不行!
本文将详细介绍我们如何利用 性能优化 "三剑客" ------ LazyForEach 、@Reusable 和 cachedCount,将列表渲染性能提升至极致。
核心问题分析
在早期的开发中,如果直接使用 ForEach 渲染列表:
typescript
// ❌ 性能较差的写法
List() {
ForEach(this.packages, (item) => {
PackageCard({ packageInfo: item })
})
}
这种方式存在两个主要缺陷:
- 全量加载 :无论列表有多长,
ForEach都会一次性创建所有的数据对象和组件节点。如果有 1000 个包裹,就会瞬间创建 1000 个PackageCard,导致内存激增。 - 频繁销毁与创建:当用户滑动列表时,移出屏幕的组件会被销毁,新进入屏幕的组件需要重新创建、布局和渲染。对于包含图片和复杂布局的卡片,这种开销是巨大的,直接导致滑动卡顿。
解决方案:性能优化 "三剑客"
1. LazyForEach:按需加载
LazyForEach 是专门为长列表设计的渲染控制语法。与 ForEach 不同,它只渲染屏幕可见区域的组件,并配合数据源(IDataSource)实现按需加载。
实现步骤:
首先,我们需要实现一个 IDataSource 接口的数据源类:
entry/src/main/ets/utils/BasicDataSource.ets
typescript
// 通用数据源基类,实现了 IDataSource 接口
export class BasicDataSource<T> implements IDataSource {
private listeners: DataChangeListener[] = [];
private originDataArray: T[] = [];
// 获取数据的总条数
public totalCount(): number {
return this.originDataArray.length;
}
// 获取指定索引的数据
public getData(index: number): T {
return this.originDataArray[index];
}
// 注册/注销监听器(框架调用)
registerDataChangeListener(listener: DataChangeListener): void {
if (this.listeners.indexOf(listener) < 0) {
this.listeners.push(listener);
}
}
unregisterDataChangeListener(listener: DataChangeListener): void {
const pos = this.listeners.indexOf(listener);
if (pos >= 0) {
this.listeners.splice(pos, 1);
}
}
// === 通知 LazyForEach 刷新 ===
notifyDataReload(): void {
this.listeners.forEach(listener => listener.onDataReloaded());
}
public setData(data: T[]) {
this.originDataArray = data;
this.notifyDataReload();
}
}
2. @Reusable:组件复用
这是解决"滑动卡顿"的关键。通过 @Reusable 装饰器,我们可以让组件具备"复用"能力。当一个列表项滑出屏幕时,它的组件实例不会被销毁,而是被放入缓存池;当新数据滑入屏幕时,直接从缓存池取出实例并更新数据,跳过了昂贵的组件创建和布局计算过程。
entry/src/main/ets/components/PackageCard.ets
typescript
@Component
@Reusable // <--- 1. 标记为可复用组件
export struct PackageCard {
@State packageInfo: PackageInfo | undefined = undefined;
/**
* 2. 复用生命周期回调
* 当组件被复用时触发。在此处更新状态变量,驱动 UI 刷新。
*
* @param params 上层传入的新参数
*/
aboutToReuse(params: Record<string, Object>) {
// 快速更新数据
this.packageInfo = params.packageInfo as PackageInfo;
// 更新其他状态
if (params.compactModeEnabled !== undefined) {
this.compactModeEnabled = params.compactModeEnabled as boolean;
}
// ...
}
build() {
// 构建复杂的卡片布局...
// 复用时,这里的节点结构保持不变,仅数据发生变化
}
}
3. cachedCount:预加载
LazyForEach 默认只加载屏幕可见的项。为了让滑动更流畅,我们可以利用 cachedCount 属性,让列表在屏幕上下方预先加载几个项目。
entry/src/main/ets/pages/PackagesPage.ets
typescript
List({ space: 12 }) {
// 使用 LazyForEach + 自定义数据源
LazyForEach(this.packagesDataSource, (packageInfo: PackageInfo, index: number) => {
ListItem() {
// 使用可复用组件
PackageCard({
packageInfo: packageInfo,
// ...
})
}
}, (item: PackageInfo) => `${item.id}_${item.updateTime}`) // 键值生成器
}
.width('100%')
.cachedCount(5) // <--- 设置缓存数量为 5
- 原理 :
cachedCount(5)表示在屏幕视口之外,预先渲染并缓存 5 个列表项。 - 收益:当用户快速滑动时,即将进入屏幕的卡片已经渲染好了,消除了白屏和闪烁,极大提升了跟手性。
优化效果对比
| 指标 | 优化前 (ForEach) | 优化后 (LazyForEach + @Reusable) | 提升原理 |
|---|---|---|---|
| 首屏加载时间 | 慢(加载所有数据) | 快(仅加载首屏可见项) | 按需渲染 |
| 内存占用 | 高(随数据量线性增长) | 低且稳定(仅维持可见项+缓存项) | 对象复用 |
| 滑动帧率 | 掉帧明显 | 满帧运行 (60/90/120Hz) | 避免频繁创建销毁节点 |
| CPU 占用 | 高(频繁 GC 和布局计算) | 低 | 复用现有节点结构 |
总结
在开发复杂列表界面时,"LazyForEach + @Reusable + cachedCount" 是标准的高性能解决方案。
- 用
LazyForEach替代ForEach,解决内存和首屏问题。 - 用
@Reusable改造子组件,解决滑动掉帧问题。 - 用
cachedCount调节预加载,进一步提升流畅度。
这套方案在 PickupPartner 项目中经受住了大量数据的考验,为用户提供了丝滑的操作体验。