ccai 2026-09-10 11:34:29 发布摘要
列表数据从几十条增长到数百条后,页面可能出现首屏变慢、快速滑动掉帧和局部修改导致全量刷新的问题。本文以车辆使用记录列表为例,说明如何区分数据处理、节点创建、状态更新和图片解码带来的成本,并介绍 LazyForEach、稳定 key、展示模型预计算和分批加载等方法。文中不提供脱离设备的通用性能承诺,测试数据应以实际项目记录为准。
一、先记录基线,而不是直接改代码
最初的列表写法通常很直接:
@State private records: VehicleRecord[] = []
build() {
Scroll() {
Column() {
ForEach(this.records, (item: VehicleRecord) => {
RecordRow({ record: item })
}, (item: VehicleRecord) => item.id)
}
}
}当数据只有几十条时,这种写法通常足够。但数据量变大后,可能出现:
- 首次进入页面等待时间增加;
- 快速上下滑动时掉帧;
- 修改一条记录却触发大量节点重新计算;
- 列表图片解码占用较多内存。
定位时应固定以下条件:
- 数据量和字段数量;
- Debug 或 Release 构建;
- 设备型号和系统版本;
- 图片尺寸和网络状态;
- 首次进入还是热启动;
- 测量次数和统计方式。
二、长列表使用惰性数据源
LazyForEach 应放在 List、Grid 或其他支持惰性加载的容器内。下面只展示数据源核心方法,监听器的完整回调名称和参数请以当前 SDK 的 DataChangeListener 定义为准。
class RecordDataSource implements IDataSource {
private records: RecordViewData[] = []
private listeners: DataChangeListener[] = []
totalCount(): number {
return this.records.length
}
getData(index: number): RecordViewData {
return this.records[index]
}
registerDataChangeListener(listener: DataChangeListener): void {
this.listeners.push(listener)
}
unregisterDataChangeListener(listener: DataChangeListener): void {
const index = this.listeners.indexOf(listener)
if (index >= 0) {
this.listeners.splice(index, 1)
}
}
update(index: number, item: RecordViewData): void {
if (index < 0 || index >= this.records.length) {
return
}
this.records[index] = item
this.listeners.forEach(listener => {
listener.onDataChange(index)
})
}
reload(items: RecordViewData[]): void {
this.records = items
// 调用当前 SDK 对应的“全量刷新”回调
// 不同版本的回调名称请以类型定义为准
}
}页面使用方式:
private dataSource: RecordDataSource = new RecordDataSource()
build() {
List() {
LazyForEach(this.dataSource, (item: RecordViewData) => {
ListItem() {
RecordRow({ record: item })
}
}, (item: RecordViewData) => item.id)
}
.cachedCount(4)
.width('100%')
.layoutWeight(1)
}这里有三个关键点:
- key 必须稳定且唯一,建议使用业务 ID;
- 修改数据后必须通知数据源监听器;
cachedCount不宜盲目调大,缓存越多,内存占用也可能越高。
三、避免在 build 中反复做复杂计算
不要在列表项构建阶段反复进行日期格式化、状态转换或复杂字符串拼接:
Text(formatDate(item.createTime))
Text(calculateStatusText(item.status))可以在数据进入 UI 前转换为展示模型:
interface RecordViewData {
id: string
vehicleName: string
displayDate: string
displayMileage: string
statusText: string
}
function toViewData(item: VehicleRecord): RecordViewData {
return {
id: item.id,
vehicleName: item.vehicleName,
displayDate: formatDate(item.createTime),
displayMileage: `${item.mileage} km`,
statusText: getStatusText(item.status)
}
}这样做不仅有助于减少 UI 阶段的计算,也可以让格式化逻辑被单独测试。
四、图片要按场景加载
列表只需要缩略图,详情页再加载原图。图片地址为空时,提供默认资源:
Image(this.record.imageUrl.length > 0
? this.record.imageUrl
: $r('app.media.default_vehicle'))
.width(72)
.height(52)
.objectFit(ImageFit.Cover)
.borderRadius(6)还应注意:
- 不要在滚动过程中重复进行复杂图片处理;
- 列表项的图片区域尽量使用固定比例;
- 网络图片要有失败占位;
- 进入详情页后再加载大图;
- 图片缓存策略要结合设备内存验证。
五、其他常见优化
如果数据解析本身很重,可以把纯计算任务放到任务池或其他后台执行能力中,但后台任务不能直接访问 UI 状态。搜索框输入也不宜每个字符都触发完整查询,可以使用短时间防抖。
同时检查以下问题:
Scroll内部是否又嵌套了可滚动列表;- 是否在每次刷新时重新创建相同的数据源;
- 是否直接改变数组却没有通知 UI;
- 是否使用数组下标作为 key;
- 是否把原图直接放进长列表;
- 是否在
build中执行网络请求或大规模 JSON 解析。
六、验证表应填真实数据
发布时建议使用如下表格,并将【待填】替换为实际测量值:
| 指标 | 优化前 | 优化后 | 测量方法 |
|---|---|---|---|
| 首次有效内容出现时间 | 【待填】 | 【待填】 | Release 构建,重复 3~5 次 |
| 快速滑动帧率或帧时间 | 【待填】 | 【待填】 | 同一设备、同一数据集 |
| 峰值内存 | 【待填】 | 【待填】 | 同一图片和缓存条件 |
| 单条记录修改影响范围 | 【待填】 | 【待填】 | 日志或 UI 检查 |
如果暂时没有性能工具或稳定数据,就写清楚“本文给出测试方案,数据待补”,不要用估算数字替代实测。
七、总结
列表优化不是简单地把 ForEach 换成 LazyForEach。真正需要同时考虑的是:
- 数据量是否适合一次性创建;
- 数据源是否正确通知变化;
- key 是否稳定;
- UI 构建阶段是否做了过多计算;
- 图片是否使用了合适的尺寸;
- 测试是否在相同条件下进行。
几十条以内的简单列表可以优先保持代码直观;当数据量、列表复杂度和局部更新频率上升后,再引入惰性数据源和更细粒度的更新机制。
相关推荐
203
0
52
0
179
0
ccai
我还没有写个人简介......
帖子
提问
粉丝
一笔加油费到月度报表:用 ArkTS 做一个离线优先的“车账本”
2026-09-18 17:38:18 发布大文件上传的断点续传:HarmonyOS 客户端与 Java 服务端协作
2026-09-18 17:36:40 发布

京公网安备:11010502051901号