页面加载性能 — 从 aboutToAppear 到异步数据加载
文章简介
页面加载速度直接影响用户对应用的第一印象。HarmonyOS 为组件和页面提供了完善的生命周期管理,开发者需要在合适的生命周期阶段执行数据加载、UI 初始化等操作,以平衡加载速度和用户体验。MoneyTrack 的首页(HomeView)、统计页(StatisticsView)和资产页(AssetsView)各自采用了不同的数据加载策略,本文系统分析它们的初始化流程优化。
核心知识点
1. 页面加载的关键路径
从用户点击页面到内容完全展示,经历了多个关键阶段:
关键路径分析:首帧渲染(步骤 B→E)决定了用户"看到东西"的速度,理想值应 < 300ms;而内容展示(步骤 J→I)决定了用户"看到有用内容"的速度,目标值应 < 1s。
2. aboutToAppear / onAppear 的执行时机
| 生命周期 | 执行时机 | 适合执行的操作 | 禁止执行的操作 |
|---|---|---|---|
| constructor | 实例创建时 | 成员变量初始化 | 任何异步操作、复杂计算 |
| aboutToAppear | build 执行前 | 轻量数据准备、发起异步请求 | 超过 10ms 的同步计算、阻塞网络请求 |
| build | 布局构建 | 纯 UI 构建 | 数据加载、业务逻辑 |
| onAppear | 首帧渲染完成后 | UI 相关后续操作、动画启动 | 耗时数据处理 |
| onPageShow | 页面每次显示时 | 页面刷新逻辑 | 重复初始化 |
核心原则:aboutToAppear只能做轻量工作(<10ms),所有耗时操作应异步执行并不等待其结果。
3. 骨架屏完整实现
骨架屏可以在数据加载完成前展示页面的"轮廓",大幅改善用户的等待感知:
@ComponentV2 struct SkeletonLoader { build() { Column() { // 骨架行 1 Row() { Circle().width(40).height(40).fill('#E8E8E8') Column() { Row().width('60%').height(14).borderRadius(7).fill('#E8E8E8') Row().width('40%').height(12).borderRadius(6).fill('#E8E8E8').margin({ top: 8 }) } .padding({ left: 12 }) } .padding(16) // 骨架行 2 Row() { Circle().width(40).height(40).fill('#E8E8E8') Column() { Row().width('70%').height(14).borderRadius(7).fill('#E8E8E8') Row().width('30%').height(12).borderRadius(6).fill('#E8E8E8').margin({ top: 8 }) } .padding({ left: 12 }) } .padding(16) } .width('100%') } } // 在页面中使用 build() { Stack() { if (this.vm.isDataLoaded) { HomeContent({ vm: this.vm }); } else { SkeletonLoader(); } } }骨架屏的关键设计要点:使用纯色矩形/圆形模拟内容布局、添加微弱的闪烁动画、数据加载完成后平滑替换为真实内容。
4. 数据预加载
在页面切换前提前加载数据可以有效提升页面打开速度。例如在 Tab 页场景中,当用户停留在首页时,提前发起统计页的数据请求:
// 当前页面中提前加载下一页数据 aboutToAppear(): void { // 加载本页数据 this.vm.initData(); // 预加载下一个页面的数据(利用空闲时间) setTimeout(() => { StatisticsVM.preloadData().catch(() => { // 预加载失败不影响当前页面 console.warn('预加载数据失败'); }); }, 500); // 等待当前页面渲染完成后预加载 }预加载策略需要权衡:提前加载能减少等待时间,但可能增加不必要的网络请求和内存占用。建议仅在用户高频访问的页面之间实施预加载。
5. 各 ViewModel initData 的对比
| ViewModel | 调用生命周期 | 同步/异步 | 数据来源 | 平均加载耗时 | 是否有缓加载机制 |
|---|---|---|---|---|---|
| HomeVM | onAppear | async/await | 本地数据库 + 网络 | 200-400ms | 是,首次加载后缓存账单报告 |
| StatisticsVM | onAppear | async/await | 本地数据库 + 网络 | 300-500ms | 是,月份切换时复用已有数据 |
| AssetsVM | aboutToAppear | async/await | 本地数据库 | 50-100ms | 否,数据量小无需缓存 |
| BillDetailVM | aboutToAppear | async/await | 数据库单条查询 | 10-30ms | 否 |
HomeVM 和 StatisticsVM 的数据加载较重,因此在onAppear中调用,确保首帧优先渲染;AssetsVM 数据量轻,可在aboutToAppear中直接调用。
6. 性能指标
衡量页面加载性能的核心指标:
| 指标名称 | 定义 | MoneyTrack 目标值 | 采集方式 |
|---|---|---|---|
| FPS(帧率) | 每秒渲染帧数 | ≥ 55 FPS | DevEco Profiler / HiAppEvent |
| 首帧时间(TTF) | 从点击到第一帧渲染完成 | ≤ 300ms | performance.mark / HiAppEvent |
| 可交互时间(TTI) | 首帧后用户可操作 | ≤ 500ms | 自定义埋点 |
| 内容加载时间 | 数据从请求到展示 | ≤ 1s | HiAppEvent STATISTIC |
| 布局耗时 | 每次 build 的布局计算时间 | ≤ 8ms | Profiler ArkUI 工具 |
7. 最佳实践
- aboutToAppear 中只做轻量准备:避免任何超过 10ms 的同步操作,异步请求发起但不 await
- 利用 onAppear 加载重数据:对于数据量大的页面,让首帧先渲染出来,数据到达后自动更新
- 骨架屏提升感知性能:不要等数据到了再渲染 UI,先显示骨架屏让用户感觉"快了"
- 预加载相邻页面数据:利用空闲时间提前加载用户可能访问的页面数据
- 持续监控性能指标:在 CI 中集成 FPS 和首帧时间检查,防止性能回归
项目代码案例
HomeView / StatisticsView / AssetsView 的初始化数据加载
HomeView(文件路径:features/home/src/main/ets/views/HomeView.ets):在onAppear中调用HomeVM.initData(),该初始化包括加载账户信息和当月账单报告。
StatisticsView(文件路径:features/statistics/src/main/ets/views/StatisticsView.ets):在onAppear中调用StatisticsVM.initData(),包括加载账户信息、获取账单报告和选中当天日期数据。
AssetsView(文件路径:features/assets/src/main/ets/views/AssetsView.ets):在aboutToAppear中调用AssetsVM.initData()。
各 ViewModel 的 initData 实现对比
// HomeVM - 监听全局刷新事件 public async initData() { await this.dataProcessing.accountProcessing.init(); await this.refreshBill(); EventBus.instance.on(EventKey.GLOBAL_REFRESH_EVENT, () => { this.refreshBill(); }); }推荐参考文档
- HarmonyOS @ComponentV2 生命周期文档
- ArkUI 异步编程(async/await)指南
- 骨架屏设计最佳实践
- DevEco Studio Profiler 性能分析指南