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,判断是否仍有主线程重活。
Logo

讨论HarmonyOS开发技术,专注于API与组件、DevEco Studio、测试、元服务和应用上架分发等。

更多推荐