HarmonyOS 长列表滚动卡顿与图片懒加载优化实战
·
HarmonyOS 长列表滚动卡顿与图片懒加载优化实战
前言
在 HarmonyOS 应用里,长列表(List / WaterFlow / Grid)是最常见的页面形态,也是性能问题的高发区。社区里两类高频提问很有代表性:一是"列表滚到底图片要等 700ms 才展示",二是"上拉加载次数越多,列表越卡"。这两个现象看似独立,本质都指向同一个问题——渲染与数据追加没有做对。本文结合 ArkUI 的渲染机制,给出可落地的优化方案与验证方法。
问题描述
- 现象一:快速滚动长列表,滚到底部后图片要延迟数百毫秒甚至 700ms 才显示,回滚到上方再下来有时又秒出。
- 现象二:进入带"上拉加载更多"的列表,加载到第 5、6 页之后,滚动明显掉帧、响应变慢,数据量越大越卡。
细节解析
一、图片延迟的本质
"滚到底图片才出来"通常是两件事叠加:① 图片是网络图,下载 + 解码需要时间;② 列表滚动时组件频繁重建,图片缓存没命中。700ms 量级往往说明解码在主线程被滚动让位,或每次都重新创建了 Image 实例、没命中内存/磁盘缓存。
关键手段:
- 给
Image设明确尺寸与objectFit,并开启异步解码(.syncLoad(false)),避免解码卡住滚动; - 用稳定的缓存 key 命中
Image内存/磁盘缓存,保证第二次进入同一 item 直接命中; - 列表里把静态子项用
.renderGroup(true)合并渲染,减少滚动时的组件重建; - 长列表必须用
LazyForEach而非一次性ForEach,只渲染可视区; - 网络图优先用服务端裁好的缩略图,原图按需加载。
二、上拉越加载越卡的本质
“上拉加载次数越多越卡”= 数据无限追加但没做节流 + 没虚拟化。每次 onReachEnd 都触发请求 / setState,组件树随数据线性增长。List / WaterFlow 本身支持懒加载,但你的"追加"逻辑在拖后腿:
- 翻页必须加锁,避免触底连发(一次滚动可能触发多次
onReachEnd); - 用
LazyForEach+ 自定义IDataSource,只渲染可视区; - 单页条数控制在 20~30 条,不要一次 concat 上百条;
- WaterFlow 用固定
columnsTemplate,避免布局反复重算。
示例代码
图片懒加载与缓存
// ImageLazy.ets
@Component
export struct ImageLazy {
private url: string;
constructor(url: string) {
this.url = url;
}
build() {
Image(this.url)
.width(120)
.height(120)
.objectFit(ImageFit.Cover)
.syncLoad(false) // 异步解码,不阻塞滚动
.cached(true) // 命中 Image 内存/磁盘缓存
.renderGroup(true) // 静态子项合并渲染,减少滚动重建
}
}
带节流锁的懒加载数据源
// PageDataSource.ets —— 继承官方 BasicDataSource 实现 IDataSource
import { BasicDataSource } from '../common/BasicDataSource';
export class PageDataSource extends BasicDataSource<string> {
private loading = false;
private cursor = 0;
// 触底加载,带节流锁,避免连发
async loadMore(fetchPage: (cursor: number) => Promise<{ items: string[]; next: number }>): Promise<void> {
if (this.loading) {
return; // 节流:上一次还没结束就直接返回
}
this.loading = true;
try {
const page = await fetchPage(this.cursor);
// 用 appendData 增量通知,而不是整段替换
for (const it of page.items) {
this.appendData(it); // 触发 LazyForEach 仅渲染新增可视项
}
this.cursor = page.next;
} finally {
this.loading = false;
}
}
}
列表绑定
// ListPage.ets
import { webview } from '@kit.ArkWeb';
@Entry
@Component
struct ListPage {
private source: PageDataSource = new PageDataSource();
private scroller: ListScroller = new ListScroller();
build() {
List({ scroller: this.scroller }) {
LazyForEach(this.source, (item: string) => {
ListItem() {
Row() { ImageLazy({ url: item }) }
}
}, (item: string) => item) // 稳定的 key,保证缓存命中
}
.onReachEnd(() => {
// 触底只发一次请求,锁在数据源内部
this.source.loadMore(fetchPage);
})
.cachedCount(5) // 预渲染可视区外的 5 屏,滑动更顺
}
}
总结
- 图片延迟:核心是"异步解码 + 稳定缓存 key + 可视化区渲染",
.syncLoad(false)、.cached(true)、.renderGroup(true)三件套配合LazyForEach基本能解决。 - 越加载越卡:核心是"节流锁 + 虚拟化 + 分页大小可控",
onReachEnd里的请求必须加锁,数据用IDataSource.appendData增量通知。 - 验证:滚回去再滚回来若秒出,说明缓存生效;开 Profiler 看触底后是否重回 60fps,判断是否仍有主线程重活。
更多推荐


所有评论(0)