在移动应用开发中,长列表是最常见的交互场景之一 —— 电商商品列表、社交信息流、新闻资讯流等都依赖高效的列表渲染能力。但在鸿蒙 ArkUI 开发中,若直接一次性加载并渲染所有列表项,会导致三大性能问题:初始化时大量 DOM 节点创建导致页面卡顿内存占用激增引发频繁 GC滚动时全量重绘造成掉帧

这里我将系统讲解鸿蒙 ArkUI 中长列表懒加载的实现方案,基于LazyForEachList组件构建 "按需加载、动态复用" 的高效列表,结合实战案例与性能优化技巧,彻底解决长列表滚动卡顿问题。

一、长列表性能瓶颈:为什么一次性加载会卡顿?

在分析解决方案前,我们先明确一次性加载所有数据的性能损耗点:

 

  1. 渲染阻塞:假设列表有 1000 项,每个项包含图片、文本、按钮等组件,一次性创建会导致 UI 线程阻塞数百毫秒,表现为页面初始化白屏或卡顿;
  2. 内存爆炸:每个组件实例(尤其是图片)会占用一定内存,1000 项可能导致内存占用增加数百 MB,触发鸿蒙系统的内存预警机制;
  3. 滚动掉帧:可视区域外的组件仍会参与布局计算和绘制,滚动时浏览器需要处理全量元素的位置更新,无法维持 60fps 的流畅帧率;
  4. GC 频繁:大量组件创建 / 销毁会导致 JavaScript 垃圾回收机制频繁触发,进一步加剧卡顿。

 

这些问题在低配置设备上尤为明显。因此,仅渲染可视区域内的列表项,并在滚动时动态加载数据成为长列表优化的核心思路。

二、ArkUI 懒加载核心方案:LazyForEach + List

鸿蒙 ArkUI 为长列表优化提供了原生支持,LazyForEach数据懒加载组件与List列表组件的组合,是官方推荐的最优解。其核心原理是只实例化可视区域内的列表项,并对滚动出视野的项进行销毁或复用

1. 技术原理解析

LazyForEach的工作机制与传统ForEach有本质区别:

 

  • 传统 ForEach:一次性遍历所有数据,创建并渲染全部组件,无论是否在可视区域;
  • LazyForEach:仅根据当前滚动位置,创建可视区域内(及前后少量缓冲项)的组件,当组件滚动出可视区域时,会被回收进入缓存池等待复用。

 

配合List组件的滚动监听能力,可实现 "滚动到指定位置时加载下一页数据" 的完整懒加载流程,整体架构如下:

 

[数据源] → [LazyForEach] → [List] → [可视区域渲染]
       ↓                     ↑
[分页加载逻辑] ← [滚动监听] ← [Scroller控制器]

2. 完整实现案例

以下是一个可直接复用的长列表懒加载实现,包含数据源封装、分页加载、滚动监听和状态管理:

(1)基础结构与数据源定义

首先定义列表项数据结构和懒加载数据源,数据源需实现IDataSource接口以支持LazyForEach

// 列表项数据类型
interface ArticleItem {
  id: string
  title: string
  summary: string
  author: string
  publishTime: string
  coverImage: string
}

// 懒加载数据源(实现IDataSource接口)
class ArticleDataSource implements IDataSource {
  private data: ArticleItem[] = []
  private listeners: ((data: ArticleItem[]) => void)[] = []

  // 获取数据总数
  totalCount(): number {
    return this.data.length
  }

  // 获取指定索引的数据
  getData(index: number): ArticleItem {
    return this.data[index]
  }

  // 注册数据变化监听
  registerDataChangeListener(listener: (data: ArticleItem[]) => void): void {
    if (!this.listeners.includes(listener)) {
      this.listeners.push(listener)
    }
  }

  // 取消监听
  unregisterDataChangeListener(listener: (data: ArticleItem[]) => void): void {
    const index = this.listeners.indexOf(listener)
    if (index !== -1) {
      this.listeners.splice(index, 1)
    }
  }

  // 追加新数据(分页加载时使用)
  append(newItems: ArticleItem[]): void {
    this.data = this.data.concat(newItems)
    // 通知数据变化,触发列表更新
    this.listeners.forEach(listener => listener(this.data))
  }

  // 清空数据
  clear(): void {
    this.data = []
    this.listeners.forEach(listener => listener(this.data))
  }
}

 

(2)主列表组件实现

使用List+LazyForEach构建列表,通过Scroller监听滚动位置,实现分页加载:

@Entry
@Component
struct ArticleListPage {
  private scroller: Scroller = new Scroller()
  private dataSource: ArticleDataSource = new ArticleDataSource()
  private currentPage: number = 1
  private pageSize: number = 15  // 每页加载15项(平衡加载次数与单次压力)
  private isLoading: boolean = false  // 防止重复请求的锁
  private hasMoreData: boolean = true  // 是否还有更多数据

  // 页面初始化时加载第一页
  aboutToAppear() {
    this.loadPageData(1)
  }

  // 加载分页数据(模拟网络请求)
  private async loadPageData(page: number) {
    if (this.isLoading || !this.hasMoreData) return
    this.isLoading = true

    try {
      // 模拟网络延迟(实际项目替换为真实接口)
      await new Promise(resolve => setTimeout(resolve, 800))

      // 生成模拟数据(实际项目替换为接口返回)
      const mockData: ArticleItem[] = Array.from({ length: this.pageSize }, (_, i) => ({
        id: `article-${page}-${i}`,
        title: `鸿蒙ArkUI性能优化实战:${(page - 1) * this.pageSize + i + 1}`,
        summary: '本文详细讲解长列表懒加载实现方案,解决滚动卡顿问题...',
        author: '鸿蒙开发者',
        publishTime: '2023-10-01',
        coverImage: `https://picsum.photos/400/200?random=${Math.random()}`
      }))

      // 添加数据到数据源
      this.dataSource.append(mockData)
      this.currentPage = page

      // 模拟数据到底(实际项目根据接口返回判断)
      if (page >= 6) {
        this.hasMoreData = false
      }
    } catch (error) {
      console.error(`第${page}页加载失败`, error)
    } finally {
      this.isLoading = false
    }
  }

  // 滚动监听:判断是否需要加载下一页
  private onScroll() {
    if (this.isLoading || !this.hasMoreData) return

    // 计算当前滚动位置与底部的距离
    const scrollOffset = this.scroller.currentOffset().yOffset
    const totalHeight = this.scroller.scrollableDistance()
    const viewportHeight = this.scroller.viewportSize().height

    // 当距离底部小于1.5个视口高度时,加载下一页
    if (totalHeight - scrollOffset < viewportHeight * 1.5) {
      this.loadPageData(this.currentPage + 1)
    }
  }

  // 列表项UI(使用@Builder提升复用效率)
  @Builder
  itemBuilder(item: ArticleItem) {
    Column() {
      Image(item.coverImage)
        .width('100%')
        .height(200)
        .objectFit(ImageFit.Cover)
      
      Column({ space: 8 }) {
        Text(item.title)
          .fontSize(18)
          .fontWeight(FontWeight.Bold)
          .maxLines(2)
          .textOverflow({ overflow: TextOverflow.Ellipsis })
        
        Text(item.summary)
          .fontSize(14)
          .color('#666666')
          .maxLines(2)
          .textOverflow({ overflow: TextOverflow.Ellipsis })
        
        Row({ space: 15 }) {
          Text(item.author)
          Text(item.publishTime)
        }
        .fontSize(12)
        .color('#999999')
      }
      .padding(15)
    }
    .backgroundColor('#ffffff')
    .borderRadius(12)
    .margin({ left: 15, right: 15, top: 10 })
    .shadow({ radius: 6, color: '#00000010', offsetY: 2 })
  }

  // 底部加载状态UI
  @Builder
  buildFooter() {
    if (this.isLoading) {
      Row({ space: 10 }) {
        LoadingProgress()
          .color('#007DFF')
          .size({ width: 24, height: 24 })
        Text('加载中...')
          .fontSize(14)
          .color('#666666')
      }
      .padding(20)
    } else if (!this.hasMoreData) {
      Text('已显示全部内容')
        .fontSize(14)
        .color('#999999')
        .padding(20)
    }
  }

  build() {
    Column() {
      List({ scroller: this.scroller }) {
        // 核心:使用LazyForEach实现懒加载
        LazyForEach(this.dataSource, (item: ArticleItem) => {
          ListItem() {
            this.itemBuilder(item)
          }
          .onAppear(() => {
            // 列表项进入可视区域时触发(可用于图片预加载)
            console.log(`Item ${item.id} 进入可视区域`)
          })
        }, (item: ArticleItem) => item.id)  // 以id作为唯一标识,优化组件复用

        // 底部加载状态
        ListItem() {
          this.buildFooter()
        }
      }
      .onScroll(() => this.onScroll())  // 绑定滚动事件
      .scrollBar(BarState.On)  // 显示滚动条,提升用户体验
      .backgroundColor('#f5f5f5')
    }
    .width('100%')
    .height('100%')
  }
}

三、关键优化点解析

上述实现已能满足基本需求,但要达到 "丝滑滚动" 的体验,还需关注以下优化细节:

1. 数据源设计:减少不必要的响应式

ArticleDataSource中的数据应使用普通 JavaScript 对象存储,而非@State装饰的响应式变量。因为LazyForEach会通过registerDataChangeListener监听数据变化,无需额外的响应式开销。

2. 列表项复用:唯一键的重要性

LazyForEach的第三个参数是键生成函数(示例中使用item.id),这个键用于标识列表项的唯一性:

LazyForEach(dataSource, item => {...}, item => item.id)

框架会根据这个键判断是否可以复用已回收的组件。若键不唯一或频繁变化,会导致组件频繁销毁重建,抵消懒加载的性能收益。

3. 滚动触发时机:平衡预加载与性能

加载下一页的触发阈值(示例中为viewportHeight * 1.5)需要根据实际场景调整:

 

  • 内容较轻(如纯文本):可设置为 1 个视口高度,减少预加载数据量;
  • 内容较重(如多图):可设置为 2-3 个视口高度,提前加载避免滚动到空白区域。

4. 加载状态管理:防止并发请求

isLoading变量作为加载锁,确保同一时间只有一个加载请求在执行,避免滚动时触发多次重复请求,浪费网络资源和内存。

四、进阶优化:从流畅到极致

在基础实现上,可通过以下技巧进一步提升性能,尤其适用于图片密集型列表:

1. 图片优化:预加载与缓存

列表项中的图片是性能瓶颈之一,可通过以下方式优化:

 

  • 预加载:在列表项onAppear事件触发时(刚进入可视区域),提前加载图片资源;
  • 缓存策略:使用@ohos.multimedia.image提供的图片缓存能力,避免重复下载;
  • 渐进式加载:先显示缩略图,再加载高清图,减少等待感。
// 图片预加载示例(在item的onAppear中调用)
preloadImage(url: string) {
  const imageSource = image.createImageSource()
  imageSource.createImageData(url).then(() => {
    console.log(`Image ${url} preloaded`)
  })
}

2. 列表项高度:固定化或预估

List组件若能提前知晓列表项高度,可更高效地计算可视区域和滚动位置:

 

  • 固定高度:若所有列表项高度一致,直接设置itemHeight

List({ scroller: this.scroller }) {
  // ...
}
.itemHeight(350)  // 固定列表项高度

预估高度:若高度不固定,可设置estimatedItemHeight提供参考值:

List({ scroller: this.scroller }) {
  // ...
}
.estimatedItemHeight(350)  // 预估高度

3. 避免滚动中的重绘

滚动过程中应避免修改可能触发 UI 重绘的状态变量(如@State@Prop):

 

  • 若需更新数据,可延迟到滚动停止后(通过onScrollStop事件);
  • 复杂动画效果应在列表项进入可视区域后再启动,避免滚动时的动画计算开销。

4. 数据请求优化

  • 请求防抖:在快速滚动时,可延迟触发加载请求(如滚动停止后 500ms 再加载);
  • 批量请求:根据设备性能动态调整pageSize(高性能设备可设 20-30,低性能设备设 10-15);
  • 错误重试:加载失败时提供重试机制,避免用户看到永久空白。

五、常见问题与解决方案

总结

鸿蒙 ArkUI 的LazyForEach + List组合为长列表优化提供了原生级支持,其核心价值在于通过按需渲染组件复用,从根源上解决了一次性加载导致的性能问题。在实际开发中,需注意:

  1. 数据源实现IDataSource接口,确保数据按需提供;
  2. 合理设置滚动触发阈值和分页大小,平衡加载效率与性能;
  3. 针对图片等重资源进行专项优化,避免成为新的性能瓶颈;
  4. 通过固定高度、减少滚动中重绘等技巧,进一步提升流畅度。

通过这套方案,即使是包含上千项的长列表,也能保持 60fps 的滚动帧率,为用户提供丝滑的交互体验。当然实际项目中,还需根据具体业务场景(如数据类型、更新频率)进行针对性调优,才能发挥最佳效果。

Logo

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

更多推荐