很多人做列表优化,第一反应是“卡了就加缓存”。但真进入项目以后,你会发现列表性能并不是一个点问题,而是一串连锁反应:数据量、渲染方式、组件复用、状态传递、滚动监测,最后都会影响用户手感。

一、长列表最怕的,不是数据多,而是渲染策略没收住

做资讯流、评论流、商品流、消息流时,大家都知道长列表容易卡。

但如果只把问题归结成“数据多”,其实不够准确。

在 ArkUI 里,真正让长列表开始发沉的,通常不是数据本身,而是下面几件事叠在一起:

  • 列表项创建过多;
  • 组件重复销毁和重建;
  • 状态更新牵动整列刷新;
  • item 没有稳定标识;
  • 滚动过程中首屏和非首屏逻辑混在一起;
  • 页面上还有图片、统计、动画等额外负担。

也就是说,长列表治理真正解决的,不是“数据多不多”,而是每次滚动时,系统到底做了多少没必要的事。

我后来把这个主题拆成四块去看:

  1. 按需加载;
  2. 列表项复用;
  3. 状态隔离;
  4. 滚动性能监测。

只要这四块梳顺,列表体验通常就会有明显变化。

二、第一步不是调参数,而是先换成更合适的渲染方式

很多人列表一开始就写成普通循环渲染,功能没问题,但数据一上来就会开始吃力。

如果场景本身就是长列表,第一步更应该先考虑按需渲染。在 ArkUI 里,这个点最典型的就是 LazyForEach。

它的价值不只是“写法不一样”,而是:

  • 不会一次性把所有项都建出来;
  • 能结合列表滚动,只处理当前需要显示的内容;
  • 后续更方便接复用与性能统计。

我这次的基础写法如下:

import { ArticleItem, getArticleList } from '../model/ArticleData'

@State private articles: ArticleItem[] = []

aboutToAppear() {
  this.articles = getArticleList(200)
}

build() {
  List({ space: 12 }) {
    LazyForEach(this.articles, (item: ArticleItem, index: number) => {
      ReuseItem({ item })
    }, (item: ArticleItem) => item.id)
  }
}

真正关键的有两个点:

  • 用 LazyForEach 替代普通批量渲染;
  • 给每个 item 提供稳定的 item.id。

很多时候,性能问题不是因为框架不行,而是因为你没有给框架稳定工作的条件。

三、稳定的 itemKey,比想象中重要得多

列表优化里有一个很容易被低估的点:唯一标识。

如果列表项没有稳定的 key,系统很难准确判断哪些项是“同一个对象”,哪些项需要真正重建。结果就是:

  • 本来可以复用的项被重新创建;
  • 状态错位;
  • 滚动中出现额外的重绘和卡顿。

所以我在列表数据设计上,会优先保证每个 item 都有固定唯一标识,不拿 index 临时凑数。

这一点在代码里看起来只是最后一个回调:

LazyForEach(this.articles, (item: ArticleItem) => {
  ReuseItem({ item })
}, (item: ArticleItem) => item.id)

但它带来的收益很实在:

  • 复用关系更稳定;
  • diff 更可靠;
  • 项目后面即使插入、删除、排序变化,列表也没那么容易乱。

四、长列表真正省下来的,不是时间,而是“重复工作”

很多优化文章喜欢直接上 FPS、耗时、掉帧率,我也会看这些数字。

但从工程角度说,我更在意的是:有没有减少重复工作。

比如这几个动作,如果没控制好,就会在滚动里被无限放大:

  • 列表项重复创建;
  • 组件重复销毁;
  • 父组件状态变化牵动全量刷新;
  • 每次小更新都触发整列表渲染。

所以我的第二层治理,是把“可复用”“可局部更新”“可状态隔离”这几件事做明确。

最直接的方式,是把列表项组件抽出来,并明确声明它是可复用组件:

@Reusable
@Component
export struct ReuseItem {
  @Prop item: ArticleItem

  build() {
    Row() {
      Column() {
        Text(this.item.title)
          .fontSize(16)
          .fontWeight(FontWeight.Medium)
          .maxLines(2)

        Text(this.item.desc)
          .fontSize(14)
          .fontColor('#666666')
          .maxLines(2)
      }
    }
    .width('100%')
    .padding(16)
    .backgroundColor(Color.White)
    .borderRadius(8)
  }
}

这类优化的意义并不神秘:

  • 同类型列表项有机会复用;
  • 创建和销毁压力会小很多;
  • 滚动时的抖动通常也会跟着下降。

我在这张 DevEco 图里故意用红色标了三类读者最该关注的点:

  • LazyForEach 的按需加载;
  • itemKey 的稳定标识;
  • @Reusable 的组件复用。

底部日志区域我也保留了“优化前 / 优化后”的对照,这比只放一段代码更容易让人形成结果感。

五、状态隔离,才是避免“无关刷新”的关键

长列表还有一个特别常见的问题:看起来是某一项状态更新,实际上整列都在跟着重刷。

这种问题初期往往不明显,一旦列表够长、每项又有图片、按钮、角标、阅读状态等内容时,问题马上就会放大。

所以我更强调状态隔离。

什么叫状态隔离?

简单说,就是:

  • 属于某个 item 的状态,尽量只在该 item 内管理;
  • 父级尽量只管理必要的列表数据;
  • 小更新尽量局部传播,不让整个列表陪跑。

比如点赞状态、展开状态、选中状态,如果本来就是局部行为,就别让它们一路往上抬成全局刷新触发器。

这件事做好了,很多时候你不用做特别复杂的性能技巧,列表就已经会顺很多。

六、没有数据监测,优化很容易变成“体感玄学”

列表优化特别容易陷入一个误区:

我觉得现在滑起来更顺了。

但“觉得”不够。

做技术干货,最好把结果落到更可验证的指标上。比如:

  • 丢帧率;
  • 平均渲染耗时;
  • FPS;
  • 首屏耗时;
  • 内存占用;
  • 可见项数量;
  • 复用项数量。

所以我会在列表页里加一个监测器,把这些关键数据收起来。最基础的做法,是在滚动回调里记录首尾 index,并结合日志持续输出:

class PerformanceMonitor {
  start() {
    console.info('start monitor')
  }

  onScroll(first: number, last: number) {
    console.info(`visible items: ${first} - ${last}`)
  }
}

当然,实际项目可以更丰富,比如配合 profiler、页面埋点或者内部统计模块。但思路是一致的:

  • 优化前先看基线;
  • 优化后再看变化;
  • 不是只看一次截图,而是看整体趋势。

这张手机图承担的是“实时看板”角色。我让它重点展示三块:

  • FPS;
  • 首屏耗时;
  • 内存占用。

同时又在文章列表区用红色虚线框和箭头解释“列表项复用”和“流畅滚动体验”。这样它就不只是展示页面,而是在讲解优化思路。

七、最终要让优化结果变得“可解释”

只说“更快了”还不够。更好的写法,是把优化前后拆解给读者看:

  • 掉帧率为什么下降;
  • 平均渲染耗时为什么缩短;
  • 内存为什么更平稳;
  • 哪些配置真正起了作用。

所以我又整理了一个“性能治理详情页”,专门把优化结果和配置开关放在一起。这样做有两个好处:

  1. 对读者来说,看得清楚;
  2. 对开发者自己来说,后续排查也方便。

这张图里我比较满意的是几处红色说明:

  • 丢帧率显著下降;
  • 渲染耗时降低;
  • FPS 接近满帧;
  • 开启稳定 itemKey;
  • 开启差量更新;
  • 开启组件复用;
  • 启用状态隔离。

你会发现,这样的图放在文章里特别适合。因为它把“优化动作”和“优化结果”同时解释掉了。

八、这类主题真正值得写透的地方

我做完这次长列表治理后,一个非常明确的感受是:

性能优化不是某个神奇参数,而是一套减少无效工作的策略。

真正能让列表变顺的,通常不是只开一个开关,而是下面这些点合在一起:

  • 用更合适的渲染方式承接长列表;
  • 保证 itemKey 稳定;
  • 让列表项可复用;
  • 把状态隔离在合理边界;
  • 用监测数据验证优化结果。

这些事情拆开看都不复杂,但只要有一个环节没收住,滚动手感就很容易开始发沉。

九、本文小记

如果让我用一句话概括这篇文章,我会写成:

长列表优化的本质,不是“把页面做快一点”,而是让渲染、状态和滚动各自回到更合理的位置。

这次我自己最认可的几个工程判断是:

  • 长列表优先考虑按需渲染;
  • itemKey 一定要稳定;
  • 组件复用是很实用的基础优化;
  • 状态隔离能显著减少无关刷新;
  • 监测数据比“体感更顺”更有说服力。

如果你接下来也要写 HarmonyOS 的性能优化题材,我会建议你别只停留在“优化前后对比”这一层,而是把渲染方式、组件复用、状态隔离和监测指标一起讲清楚。这样文章不只是有结果,也会更有技术味。

Logo

作为“人工智能6S店”的官方数字引擎,为AI开发者与企业提供一个覆盖软硬件全栈、一站式门户。

更多推荐