鸿蒙掌上驾考宝典应用开发41:鸿蒙应用性能优化——从渲染窗口到大数据列表
第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 设置
更多推荐




所有评论(0)