第41篇:鸿蒙应用性能优化——从渲染窗口到大数据列表

在这里插入图片描述

一、引言

性能优化是应用开发中不可忽视的关键环节。在移动设备上,流畅的用户体验直接决定了应用的留存率。DriverLicenseExam 项目虽然是一个驾考模板,但在性能优化方面做了大量工作:从渲染窗口机制到大数据列表渲染,从状态管理优化到内存泄漏防范,每一个环节都有值得学习的优化策略。本文将结合项目源码,系统梳理 ArkUI 应用性能优化的最佳实践。

二、渲染窗口优化——只渲染你看得见的

2.1 问题背景

在考试页面(Exam 组件)中,Swiper 组件会管理所有的试题页面。如果题库数量很大(比如上千道题),一次性渲染所有页面会导致严重的性能问题。

2.2 渲染窗口机制

Exam 组件通过 renderWindowSize 实现了渲染窗口机制:

// Exam.ets - 渲染窗口优化
private readonly renderWindowSize: number = 2;

shouldRenderQuestion(examIndex: number): boolean {
  return Math.abs(examIndex - this.currentIndex) <= this.renderWindowSize;
}

build() {
  Swiper(examController.swiperController) {
    ForEach(this.examManager.examDetails, (item: ExamDetail, examIndex: number) => {
      Column({ space: 12 }) {
        if (this.shouldRenderQuestion(examIndex)) {
          // 只渲染当前窗口内的题目
          SelectComponent({
            item: item,
            changeCorrectNum: (isAdd: boolean) => {
              isAdd ? this.examManager.correctNumber++ : this.examManager.correctNumber--;
            },
          });
        } else {
          // 窗口外的题目渲染空白占位
          Column().width('100%').height('100%');
        }
      }
      .width('100%')
      .height('100%');
    });
  }
}

这个机制的原理是:当 renderWindowSize = 2 时,只渲染当前题目前后各 2 道题,一共 5 道题在前端。用户滑动 Swiper 时,随着 currentIndex 的变化,动态切换需要渲染的题目。

性能提升分析:

假设题库有 1000 道题:

  • 优化前:Swiper 预加载所有页面,渲染 1000 个 SelectComponent 实例
  • 优化后:只渲染 5 个 SelectComponent 实例

渲染量减少了 200 倍,这对内存占用和首屏加载速度的提升是巨大的。

三、列表渲染优化——用好 ForEach 的 key

3.1 key 的重要性

在 ArkUI 中,ForEach 使用 key 来标识每个列表项,框架通过 key 来判断哪些项需要新增、更新或删除。如果 key 设置不当,会导致整个列表频繁重建,严重影响性能。

// 不推荐:使用 index 作为 key
ForEach(this.examManager.examDetails, (item: ExamDetail, index: number) => {
  ListItem() { ... }
}, (item: ExamDetail, index: number) => index.toString());

// 推荐:使用唯一标识作为 key
ForEach(this.videoList, (item: Video) => {
  ListItem() { ... }
}, (item: Video) => JSON.stringify(item));

// 最佳实践:使用 item 的唯一 ID 字段
ForEach(this.examManager.examDetails, (item: ExamDetail) => {
  ListItem() { ... }
}, (item: ExamDetail) => item.question);

3.2 使用稳定 key 的好处

稳定的 key 可以帮助 ForEach 实现最小化更新

  • 新增项 → 只渲染新增的 ListItem
  • 删除项 → 只移除对应的 ListItem
  • 重排序 → 移动 DOM 节点而非重新创建

如果使用 index 作为 key,数据重排会导致所有项重建,因为 index 发生了变化。

四、Swiper 缓存策略

4.1 cachedCount 的权衡

Swiper 组件的 cachedCount 属性控制预加载页面的数量:

// 场景1:不缓存 - 适合页面内容轻量的场景
Swiper(this.swiperController)
  .cachedCount(0)  // 不预加载任何页面

// 场景2:适量缓存 - 适合页面渲染成本高的场景
Swiper(this.swiperController)
  .cachedCount(1)  // 预加载前后各 1 页

项目中根据实际需求选择了 cachedCount(0),原因如下:

  • 选择题组件的渲染成本较低
  • 渲染窗口机制已经确保了页面切换的流畅性
  • 不缓存可以节省内存占用

4.2 禁用不需要的特性

Swiper(this.swiperController)
  .cachedCount(0)
  .index(this.currentTab)
  .loop(false)        // 禁用循环播放
  .indicator(false);  // 隐藏导航点指示器

禁用不必要的特性可以减少框架的额外计算开销。

五、状态管理优化

5.1 避免不必要的 @Monitor 触发

// HomeView.ets
@Monitor('didCount')
drawProgress() {
  // 使用 Canvas 重绘进度条
  const width = this.context.width;
  this.context.clearRect(0, 0, width, this.context.height);
  
  const doneWidth = width * this.didCount / this.totalCount;
  this.context.beginPath();
  this.context.strokeStyle = '#64BB5C';
  this.context.lineWidth = 2;
  this.context.moveTo(0, 5);
  this.context.lineTo(doneWidth, 5);
  this.context.stroke();
}

这里的 @Monitor('didCount') 是一个精细化的监听,它只监听 didCount 一个变量的变化,不会因为其他变量的变化而触发重绘。

5.2 @Trace 的最小化标注

@ObservedV2
export class ExamService {
  @Trace mockExamCount: number = 0;   // 被 @Trace 标注,可观察
  @Trace mockExamScore: number[] = []; // 被 @Trace 标注,可观察
  private examDetails: ExamDetail[] = []; // 未被标注,不可观察
}

@Trace 装饰器是有开销的,它需要建立响应式依赖追踪。因此,只对需要 UI 更新的字段使用 @Trace。像 examDetails 这种只在服务内部使用的数据,就不需要 @Trace 标注。

5.3 AppStorage 的合理使用

// 适当使用 AppStorage 存储全局状态
AppStorage.setOrCreate('bottomRectHeight', bottomRectHeight);
AppStorage.setOrCreate('topRectHeight', topRectHeight);

// 避免过度使用全局状态
// 不推荐:将组件内部状态提升到 AppStorage
AppStorage.setOrCreate('componentInternalState', value);

全局状态(AppStorage)适合存储跨组件、跨页面共享的数据。组件内部状态应该使用 @Local 管理,避免全局状态带来的维护复杂度。

六、组件懒加载

6.1 @Builder 延迟加载

// Exam.ets - 使用 @Builder 延迟构建弹窗内容
@Builder
exitExamDialog() {
  Column() {
    Text('确定要退出考试吗?').fontSize(16);
    Text('本次答题记录将被清空').fontSize(14).fontColor('#999');
    Row() {
      Button('继续答题')
        .onClick(() => this.examExitDialog.close());
      Button('确定退出')
        .onClick(() => this.exitExam());
    }
  }
  .padding(24);
}

弹窗内容使用 @Builder 构建,只有在弹窗打开时才会实际渲染,不会在页面初始化时加载。

七、内存管理最佳实践

7.1 清理定时器

aboutToAppear(): void {
  if (this.examManager.timeLimit !== 0) {
    this.timerId = setInterval(() => {
      if (!this.isStop) {
        if (this.remainTime >= 1) {
          this.remainTime--;
        } else {
          this.isStop = true;
          this.examSubmitDialog.open();
        }
      }
    }, 1000);
  }
}

aboutToDisappear(): void {
  // 清理定时器,防止内存泄漏
  if (this.timerId) {
    clearInterval(this.timerId);
  }
  // 清理选中状态
  this.examManager.clearSelectedOption();
}

aboutToDisappear 中清理定时器和状态,是防止内存泄漏的关键步骤。

八、总结

DriverLicenseExam 项目展示的性能优化实践涵盖了以下几个方面:

优化策略 核心方法 效果
渲染窗口 renderWindowSize + shouldRenderQuestion 减少 99% 的不必要渲染
key 优化 稳定唯一的 key 避免列表项重建
Swiper 缓存 cachedCount(0) 节省内存占用
状态标注 最小化 @Trace 减少响应式开销
延迟加载 @Builder 构建弹窗 按需渲染
资源清理 aboutToDisappear 清理 防止内存泄漏

这些优化策略的核心理念是:只渲染用户能看到的内容,只计算需要更新的状态。在 ArkUI 中,框架已经做了大量优化工作,但开发者仍然需要理解渲染机制,避免写出性能不佳的代码。


关键源码文件:

  • components/exam/src/main/ets/components/Exam.ets — 渲染窗口优化
  • commons/datasource/src/main/ets/ExamService.ets — @Trace 标注
  • products/entry/src/main/ets/pages/home/HomeView.ets — @Monitor 监听
  • products/entry/src/main/ets/entryability/EntryAbility.ets — AppStorage 设置
Logo

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

更多推荐