HarmonyOS 鸿蒙Next数学类上架项目解析32-列表渲染性能与内存优化

HarmonyOS 鸿蒙Next数学类上架项目解析32-列表渲染性能与内存优化 在开发鸿蒙数学计算应用时,遇到列表性能问题:

  1. 长列表滚动卡顿
  2. 大量数据渲染导致内存占用过高
  3. 列表项更新时整个列表重新渲染
  4. 快速滚动时出现白屏或闪烁

如何优化ArkTS应用中的列表渲染性能和内存占用?

4 回复

目前有明确的文档介绍。列表高性能实现方式一般有以下方式:

  1. 使用@Reusable + LazyForEach,或者@Reusable + Repeat。
  2. 使用@ReusableV2 + LazyForEach,或者@ReusableV2 + Repeat,这套组合也是推荐的。

上面两种是列表处理方式。不过列表卡顿,还得考虑item的组件渲染性能,item里面是否考虑到组件复用,组件层级是否可降低,组件是否可进行最小化渲染等。

文档参考:长列表加载丢帧优化-界面渲染性能优化-性能场景优化案例-性能 - 华为HarmonyOS开发者 (huawei.com)

文档参考:Repeat-状态管理与渲染控制-ArkTS组件-ArkUI(方舟UI框架)-应用框架 - 华为HarmonyOS开发者 (huawei.com)

更多关于HarmonyOS 鸿蒙Next数学类上架项目解析32-列表渲染性能与内存优化的实战系列教程也可以访问 https://www.itying.com/category-93-b0.html


列表性能问题的核心是减少不必要的渲染和内存占用。需要使用懒加载、虚拟列表、正确的key策略等优化手段。

1. 常见性能问题分析

// 问题1:没有使用唯一key,导致整个列表重新渲染

@Component
struct BadList {
  @State items: string[] = []

  build() {
    List() {
      // 错误:使用index作为key,删除/插入时会导致大量重渲染
      ForEach(this.items, (item: string, index: number) => {
        ListItem() {
          Text(item)
        }
      }, (item: string, index: number) => index.toString())  // 错误的key
    }
  }
}

// 问题2:列表项组件过于复杂

@Component
struct HeavyListItem {
  @Prop data: ComplexData

  build() {
    Column() {
      // 大量嵌套和计算,每次渲染都很耗时
      ForEach(this.data.subItems, (sub: SubItem) => {
        Row() {
          Image(sub.icon).width(50).height(50)
          Column() {
            Text(sub.title)
            Text(this.formatDate(sub.date))  // 每次渲染都计算
            Text(this.calculatePrice(sub))   // 每次渲染都计算
          }
        }
      })
    }
  }
}

// 问题3:一次性加载所有数据

@Component
struct LoadAllAtOnce {
  @State items: DataItem[] = []

  aboutToAppear(): void {
    // 一次性加载10000条数据,内存爆炸
    this.items = this.loadAllData()
  }
}

2. 使用LazyForEach实现懒加载

  //  数据源接口实现
class ListDataSource implements IDataSource {
  private data: DataItem[] = []
  private listeners: DataChangeListener[] = []

  constructor(data: DataItem[]) {
    this.data = data
  }

  totalCount(): number {
    return this.data.length
  }

  getData(index: number): DataItem {
    return this.data[index]
  }

  registerDataChangeListener(listener: DataChangeListener): void {
    if (this.listeners.indexOf(listener) < 0) {
      this.listeners.push(listener)
    }
  }

  unregisterDataChangeListener(listener: DataChangeListener): void {
    const index = this.listeners.indexOf(listener)
    if (index >= 0) {
      this.listeners.splice(index, 1)
    }
  }

  // 通知数据变化
  notifyDataReload(): void {
    this.listeners.forEach(listener => {
      listener.onDataReloaded()
    })
  }

  notifyDataAdd(index: number): void {
    this.listeners.forEach(listener => {
      listener.onDataAdd(index)
    })
  }

  notifyDataChange(index: number): void {
    this.listeners.forEach(listener => {
      listener.onDataChange(index)
    })
  }

  notifyDataDelete(index: number): void {
    this.listeners.forEach(listener => {
      listener.onDataDelete(index)
    })
  }

  // 数据操作方法
  addData(item: DataItem): void {
    this.data.push(item)
    this.notifyDataAdd(this.data.length - 1)
  }

  deleteData(index: number): void {
    this.data.splice(index, 1)
    this.notifyDataDelete(index)
  }

  updateData(index: number, item: DataItem): void {
    this.data[index] = item
    this.notifyDataChange(index)
  }

  reloadData(newData: DataItem[]): void {
    this.data = newData
    this.notifyDataReload()
  }
}

interface DataItem {
  id: string
  title: string
  content: string
}

// 使用LazyForEach
@Entry
@Component
struct OptimizedList {
  private dataSource: ListDataSource = new ListDataSource([])

  aboutToAppear(): void {
    // 初始化数据
    const initialData: DataItem[] = []
    for (let i = 0; i < 10000; i++) {
      initialData.push({
        id: `item_${i}`,
        title: `标题 ${i}`,
        content: `内容 ${i}`
      })
    }
    this.dataSource = new ListDataSource(initialData)
  }

  build() {
    List() {
      // LazyForEach只渲染可见区域的项
      LazyForEach(this.dataSource, (item: DataItem) => {
        ListItem() {
          OptimizedListItem({ item: item })
        }
      }, (item: DataItem) => item.id)  // 使用唯一ID作为key
    }
    .width('100%')
    .height('100%')
    .cachedCount(5)  // 缓存前后5个项
  }
}

3. 列表项组件优化

 //优化的列表项组件
@Component
struct OptimizedListItem {
  @Prop item: DataItem

  // 缓存计算结果
  @State private formattedDate: string = ''

  aboutToAppear(): void {
    // 只在组件创建时计算一次
    this.formattedDate = this.formatDate(this.item.timestamp)
  }

  private formatDate(timestamp: number): string {
    const date = new Date(timestamp)
    return `${date.getFullYear()}-${date.getMonth() + 1}-${date.getDate()}`
  }

  build() {
    Row() {
      // 使用固定尺寸,避免布局计算
      Column() {
        Text(this.item.title)
          .fontSize(16)
          .fontWeight(FontWeight.Medium)
          .maxLines(1)
          .textOverflow({ overflow: TextOverflow.Ellipsis })
        
        Text(this.item.content)
          .fontSize(14)
          .fontColor('#666666')
          .maxLines(2)
          .textOverflow({ overflow: TextOverflow.Ellipsis })
          .margin({ top: 4 })
        
        Text(this.formattedDate)
          .fontSize(12)
          .fontColor('#999999')
          .margin({ top: 4 })
      }
      .layoutWeight(1)
      .alignItems(HorizontalAlign.Start)
    }
    .width('100%')
    .height(80)  // 固定高度,提升滚动性能
    .padding(12)
  }
}
interface DataItem {
  id: string
  title: string
  content: string
  timestamp: number
}

4. 分页加载

  //分页加载管理器
class PaginationManager {
  private pageSize: number = 20
  private currentPage: number = 0
  private hasMore: boolean = true
  private isLoading: boolean = false

  constructor(pageSize: number = 20) {
    this.pageSize = pageSize
  }

   // 加载下一页
  async loadNextPage(
    loadFn: (page: number, size: number) => Promise<DataItem[]>
  ): Promise<DataItem[]> {
    if (this.isLoading || !this.hasMore) {
      return []
    }
    
    this.isLoading = true
    
    try {
      const data = await loadFn(this.currentPage, this.pageSize)
      
      if (data.length < this.pageSize) {
        this.hasMore = false
      }
      
      this.currentPage++
      return data
    } finally {
      this.isLoading = false
    }
  }

   // 重置分页
  reset(): void {
    this.currentPage = 0
    this.hasMore = true
    this.isLoading = false
  }

   // 是否还有更多数据
  canLoadMore(): boolean {
    return this.hasMore && !this.isLoading
  }
}

@Entry
@Component
struct PaginatedList {
  @State items: DataItem[] = []
  @State isLoading: boolean = false
  @State hasMore: boolean = true
  private pagination: PaginationManager = new PaginationManager(20)

  aboutToAppear(): void {
    this.loadMore()
  }

  async loadMore(): Promise<void> {
    if (!this.pagination.canLoadMore()) return
    
    this.isLoading = true
    
    const newItems = await this.pagination.loadNextPage(
      async (page, size) => {
        // 模拟API请求
        await new Promise(resolve => setTimeout(resolve, 500))
        
        const items: DataItem[] = []
        const start = page  size
        for (let i = 0; i < size; i++) {
          items.push({
            id: `item_${start + i}`,
            title: `标题 ${start + i}`,
            content: `内容 ${start + i}`,
            timestamp: Date.now()
          })
        }
        return items
      }
    )
    
    this.items = [...this.items, ...newItems]
    this.hasMore = this.pagination.canLoadMore()
    this.isLoading = false
  }

  build() {
    List() {
      ForEach(this.items, (item: DataItem) => {
        ListItem() {
          OptimizedListItem({ item: item })
        }
      }, (item: DataItem) => item.id)
      
      // 加载更多指示器
      if (this.hasMore) {
        ListItem() {
          Row() {
            if (this.isLoading) {
              LoadingProgress().width(24).height(24)
              Text('加载中...').margin({ left: 8 })
            } else {
              Text('上拉加载更多')
            }
          }
          .width('100%')
          .height(50)
          .justifyContent(FlexAlign.Center)
        }
      }
    }
    .width('100%')
    .height('100%')
    .onReachEnd(() => {
      this.loadMore()
    })
  }
}

5. 列表项复用与缓存

  //图片缓存管理
class ImageCache {
  private static cache: Map<string, string> = new Map()
  private static maxSize: number = 100

  static get(url: string): string | undefined {
    return this.cache.get(url)
  }

  static set(url: string, data: string): void {
    // LRU策略:超出限制时删除最早的
    if (this.cache.size >= this.maxSize) {
      const firstKey = this.cache.keys().next().value
      this.cache.delete(firstKey)
    }
    this.cache.set(url, data)
  }

  static clear(): void {
    this.cache.clear()
  }
}

  //带缓存的列表项
@Component
struct CachedListItem {
  @Prop item: ItemWithImage
  @State imageLoaded: boolean = false

  aboutToAppear(): void {
    // 检查缓存
    const cached = ImageCache.get(this.item.imageUrl)
    if (cached) {
      this.imageLoaded = true
    }
  }

  build() {
    Row() {
      // 图片占位符
      Stack() {
        if (this.imageLoaded) {
          Image(this.item.imageUrl)
            .width(60)
            .height(60)
            .borderRadius(8)
        } else {
          Column()
            .width(60)
            .height(60)
            .backgroundColor('#f0f0f0')
            .borderRadius(8)
        }
      }
      
      Column() {
        Text(this.item.title)
          .fontSize(16)
        Text(this.item.subtitle)
          .fontSize(14)
          .fontColor('#666666')
      }
      .margin({ left: 12 })
      .alignItems(HorizontalAlign.Start)
    }
    .width('100%')
    .padding(12)
  }
}
interface ItemWithImage {
  id: string
  title: string
  subtitle: string
  imageUrl: string
}

6. 虚拟滚动优化

/ 虚拟滚动列表 只渲染可见区域的项,大幅减少DOM节点 /

@Entry
@Component
struct VirtualScrollList {
  @State visibleItems: DataItem[] = []
  @State scrollOffset: number = 0

  private allItems: DataItem[] = []
  private itemHeight: number = 80
  private containerHeight: number = 600
  private bufferCount: number = 5

  aboutToAppear(): void {
    // 初始化大量数据
    for (let i = 0; i < 10000; i++) {
      this.allItems.push({
        id: `item_${i}`,
        title: `标题 ${i}`,
        content: `内容 ${i}`,
        timestamp: Date.now()
      })
    }
    this.updateVisibleItems()
  }

   //  计算可见项
  updateVisibleItems(): void {
    const startIndex = Math.max(0,
      Math.floor(this.scrollOffset / this.itemHeight) - this.bufferCount
    )
    const endIndex = Math.min(
      this.allItems.length,
      Math.ceil((this.scrollOffset + this.containerHeight) / this.itemHeight) + this.bufferCount
    )
    
    this.visibleItems = this.allItems.slice(startIndex, endIndex).map((item, index) => ({
      ...item,
      _virtualIndex: startIndex + index
    }))
  }

  build() {
    Scroll() {
      Column() {
        // 顶部占位
        Column()
          .height(this.getTopPadding())
        
        // 只渲染可见项
        ForEach(this.visibleItems, (item: DataItem & { _virtualIndex: number }) => {
          Row() {
            Text(item.title)
              .fontSize(16)
          }
          .width('100%')
          .height(this.itemHeight)
          .padding(12)
          .backgroundColor(item._virtualIndex % 2 === 0 ? '#ffffff' : '#f9f9f9')
        }, (item: DataItem) => item.id)
        
        // 底部占位
        Column()
          .height(this.getBottomPadding())
      }
    }
    .width('100%')
    .height(this.containerHeight)
    .onScroll((xOffset: number, yOffset: number) => {
      this.scrollOffset += yOffset
      this.updateVisibleItems()
    })
  }

  getTopPadding(): number {
    const startIndex = Math.max(0,
      Math.floor(this.scrollOffset / this.itemHeight) - this.bufferCount
    )
    return startIndex  this.itemHeight
  }

  getBottomPadding(): number {
    const endIndex = Math.min(
      this.allItems.length,
      Math.ceil((this.scrollOffset + this.containerHeight) / this.itemHeight) + this.bufferCount
    )
    return (this.allItems.length - endIndex)  this.itemHeight
  }
}

7. 性能监控

  //列表性能监控
class ListPerformanceMonitor {
  private renderTimes: number[] = []
  private scrollFPS: number[] = []
  private lastFrameTime: number = 0

   //记录渲染时间
  recordRenderTime(time: number): void {
    this.renderTimes.push(time)
    if (this.renderTimes.length > 100) {
      this.renderTimes.shift()
    }
  }

   //记录滚动帧率
  recordScrollFrame(): void {
    const now = Date.now()
    if (this.lastFrameTime > 0) {
      const fps = 1000 / (now - this.lastFrameTime)
      this.scrollFPS.push(fps)
      if (this.scrollFPS.length > 60) {
        this.scrollFPS.shift()
      }
    }
    this.lastFrameTime = now
  }
   //获取平均渲染时间
  getAverageRenderTime(): number {
    if (this.renderTimes.length === 0) return 0
    return this.renderTimes.reduce((a, b) => a + b, 0) / this.renderTimes.length
  }

   //   获取平均帧率
  getAverageFPS(): number {
    if (this.scrollFPS.length === 0) return 0
    return this.scrollFPS.reduce((a, b) => a + b, 0) / this.scrollFPS.length
  }

      // 输出性能报告
  report(): void {
    console.info(`[性能报告] 平均渲染时间: ${this.getAverageRenderTime().toFixed(2)}ms`)
    console.info(`[性能报告] 平均帧率: ${this.getAverageFPS().toFixed(1)} FPS`)
  }
}

总结

列表渲染性能优化的关键点:

  1. 使用LazyForEach实现懒加载,只渲染可见项
  2. 使用唯一且稳定的key,避免不必要的重渲染
  3. 列表项使用固定高度,提升滚动性能
  4. 缓存计算结果,避免重复计算
  5. 实现分页加载,避免一次性加载大量数据
  6. 使用图片缓存,减少重复加载
  7. 设置合适的cachedCount缓存数量
  8. 监控性能指标,持续优化
  9. 项目链接:https://gitee.com/solgull/math-fbox

鸿蒙Next列表渲染性能优化主要涉及懒加载、列表项复用和虚拟化技术。内存优化策略包括使用轻量级组件、避免频繁创建对象、及时释放无用资源。开发者应合理使用ForEach渲染,控制列表项数量,优化数据结构和渲染逻辑。

针对HarmonyOS Next中ArkTS列表渲染的性能与内存优化,核心在于利用框架提供的懒加载、复用机制和精细化更新能力。以下是具体优化方案:

1. 长列表卡顿优化

  • 使用LazyForEach替代ForEach进行数据源绑定,仅渲染可视区域内的列表项,大幅减少首次渲染和滚动时的节点数量。
  • 确保LazyForEachListItemListItemGroup配合使用,并正确实现onAppear/onDisappear生命周期回调,及时释放不可见项的资源。

2. 内存占用过高处理

  • 通过cachedCount属性(如List组件)设置合理的预加载项数,平衡滚动流畅性与内存开销,避免缓存过多不可见项。
  • 对于复杂列表项,使用@Reusable装饰器标记可复用的自定义组件,配合框架的组件复用机制减少内存重复分配。
  • aboutToReuseaboutToRecycle回调中手动管理重用时的高开销资源(如图片内存释放)。

3. 避免全列表重新渲染

  • 为列表项组件添加key参数,确保数据更新时框架能精准定位差异项,仅更新必要节点。
  • 使用状态管理(如@State@Link)的局部更新特性,避免顶层数据变更触发整个列表刷新。
  • 对于动态列表,采用List组件的onScrollIndex事件监听可视区域变化,实现数据分片加载与更新。

4. 滚动白屏/闪烁解决

  • 启用listDirectionlayoutWeight优化布局计算,减少滚动时的布局重排。
  • 为图片资源设置合适尺寸或使用Imageinterpolation控制加载质量,避免滚动时解码阻塞。
  • 通过blankCount属性(瀑布流等场景)预留滚动占位空间,配合LazyForEach保持滚动连续性。

补充建议

  • 复杂列表项应拆分为独立组件,结合@Component@Builder按需构建内容。
  • 使用performance模块监控滚动帧率与内存峰值,针对性调整缓存策略。
  • 对于超长列表,考虑数据分页加载或虚拟滚动方案,进一步降低渲染压力。

以上方法直接基于ArkUI框架能力,可系统性提升列表性能与内存效率。

回到顶部