Flutter 鸿蒙性能优化实战:把非关键初始化移到首帧之后

启动慢不一定是 Flutter 引擎慢,很多时候是应用在第一帧之前做了太多工作:读取配置、解析缓存、初始化日志、请求远端数据,甚至提前创建整个列表。本文项目记录首帧时间,然后在首帧后延迟加载模拟数据。

项目地址:flutter_ohos_startup_demo。首帧入口会在测量口径明确后展示。

一、实测环境和测量方式

项目实测版本或条件
Flutter OH3.44.9+ohos-0.0.1-canary1
Dart3.12.2
DevEco Studio26.0.0 Release
HarmonyOS7.0.0.105 (API 26)
构建模式profile
延后数据24 条模拟数据,首帧后加载

本文记录的是 Flutter 页面首帧,不把它当成从桌面点击图标开始的系统冷启动;系统冷启动需要平台侧时间戳单独验证。

WidgetsBinding.instance.addPostFrameCallback((_) {
  if (mounted) setState(() => firstFrameMs = watch.elapsedMilliseconds);
});

数据加载在 addPostFrameCallback 之后继续执行,页面先展示必要的标题和进度状态。日志格式是 STARTUP_BENCHMARK firstFrameMs=... dataItems=...。这个时间是 Flutter 页面首帧,不等于操作系统从点击图标到应用进程可交互的冷启动时间,文章中必须把两个口径分开。

真正项目里我会先列出首屏必需数据和可延后数据。用户一打开就要看到的账号、权限或关键状态不能为了数字全部延迟;日志、非首屏图片、下一页数据和低优先级缓存可以拆到首帧后。延迟还要配合 loading、取消和错误处理,不能让用户看到空白页。

冷启动测量需要 profile 或 release 构建、固定设备、重复多次,并区分冷启动和热启动。不要在 debug 模式只测一次,然后把最好结果写进文章。建议同时记录首帧、首个可交互时间、关键内容出现时间和进程内存。

仓库已通过分析、Widget 测试和 HAP 构建。本文保留首帧和延后数据两张真机页面图;DevTools 时间线和多次采样表仍需按冷启动、热启动分别补齐。若数据加载失败,页面应保留错误提示和重试按钮,这同样是性能方案的一部分,因为失败重试不能阻塞首屏。

这篇文章的核心结论是:先把首屏工作分层,再测量每层的真实价值。addPostFrameCallback 只是工具,不是延迟一切工作的借口。正确的首屏优化应该同时改善可感知速度和数据可靠性。

二、先看容易出问题的启动写法

1. 首帧时间到底从哪里开始

本项目在 initState 创建 Stopwatch,再通过 WidgetsBinding.instance.addPostFrameCallback 记录 Flutter 页面完成首帧的时间。这个口径只覆盖 Flutter 页面从开始构建到首帧回调的过程,不包含用户点击桌面图标、Ability 创建、引擎加载和系统窗口准备。因此页面显示的 firstFrameMs 不能直接当成操作系统冷启动时间。

我把 24 条模拟数据放到首帧之后延迟加载。首帧阶段只展示标题、首帧状态和说明文字;延迟任务完成后再填充列表。这样做的价值不是让数据消失,而是把“用户必须马上看到的内容”和“可以晚一点出现的内容”分开。真实业务中,账号、权限和关键状态不能为了追求一个好看的数字而盲目延后。

2. 延后加载也要有完整状态

一个合格的启动优化方案至少要考虑加载中、成功、失败和重试。首帧之后的任务可能被页面销毁、网络断开或系统回收打断,不能只写一个 Future.delayed 就认为问题解决了。需要异步加载时,应配合取消、超时、错误提示和重试入口;否则首屏虽然快了,用户却会遇到空白页或无法恢复的失败状态。

3. 真正上线前怎么测

冷启动和热启动必须分开,debug、profile 和 release 也必须分开。建议固定设备、刷新率、后台应用和测试数据,连续测量首帧、首个可交互时间、关键内容出现时间及进程 PSS。页面首帧可以由 Flutter 日志记录,系统冷启动则需要平台侧时间戳或自动化工具,两者不要混用。

三、换成首帧后的延迟初始化

1. 先列出首屏的工作清单

启动优化不是把所有初始化都塞进 addPostFrameCallback。我会先把工作分成三类:首屏绘制前必须完成的工作、首帧后马上需要的工作、进入下一页或空闲时才需要的工作。权限状态、账号信息和安全配置通常属于第一类;日志、非首屏图片、下一页数据和统计上报通常可以后移。

容易出问题的写法是把所有任务串在 runApp 之前:

Future<void> main() async {
  WidgetsFlutterBinding.ensureInitialized();
  await loadConfig();
  await warmUpDatabase();
  await loadAllHomeData();
  runApp(const App());
}

更合适的写法是让页面先进入可交互状态,再启动可延后任务,并保留成功、失败和重试状态:


void initState() {
  super.initState();
  WidgetsBinding.instance.addPostFrameCallback((_) {
    if (mounted) unawaited(_loadDeferredData());
  });
}

这不是让错误消失,而是让错误出现在已经可操作的页面里。延后任务仍要有超时、取消和错误提示,不能因为首帧变快就牺牲可恢复性。

2. 需要区分四个时间点

文章记录的 firstFrameMs 只表示 Flutter 页面首帧回调。实际体验还包括系统启动进程、Flutter 引擎就绪、首个可交互控件出现和关键内容加载完成。建议在日志里分别记录:

时间点含义采集位置
Ability 启动系统开始创建应用平台侧时间戳
Flutter 首帧页面完成第一帧addPostFrameCallback
首个可交互用户可以点击或滑动页面状态回调
关键内容完成账号、首页数据等到达业务数据层

只优化其中一个时间点,不能直接宣称“冷启动提升”。

3. 首屏拆分不是延迟一切

有些工作必须在首帧之前完成。例如权限状态决定页面是否能显示,账号信息决定用户是否能进入主流程,安全配置决定请求是否可以发出。把这些工作全部放到首帧后,虽然日志里的 firstFrameMs 可能变小,但用户会先看到错误页面,再等待页面跳转,实际体验反而更差。

建议为每项初始化写出“最晚完成时间”:首帧前、首个可交互前、关键内容出现前或进入下一页前。再把可延后任务设计为可取消的 Future,并在 State 销毁后停止更新:

Future<void> _loadDeferredData() async {
  setState(() => status = LoadStatus.loading);
  try {
    final items = await repository.loadItems();
    if (!mounted) return;
    setState(() {
      data = items;
      status = LoadStatus.ready;
    });
  } catch (_) {
    if (!mounted) return;
    setState(() => status = LoadStatus.failed);
  }
}

4. 延后任务的用户状态

首帧后加载至少要提供加载中、成功、失败和重试四种状态。加载中状态需要有稳定的布局占位,避免列表到达后整个页面跳动;失败状态需要让用户可以重试,不能只在 HiLog 中打印异常。若任务本身不可取消,页面退出后也必须忽略结果,避免旧页面触发 setState

四、真机操作和结果边界

1. 冷启动采样的操作序列

测试时先停止应用并清理进程,再从桌面启动;等待首个可交互控件出现后记录时间,最后等待延后数据完成。冷启动和热启动分别记录,不能把任务管理器恢复的进程当成冷启动。每种模式至少采样 5 次,报告中位数和 P95,并保留原始日志。

页面首帧优化如果只在 debug 模式有效,通常是诊断开销被延后了;profile 和 release 才能说明真实构建的趋势。系统冷启动还需结合 ArkTS/Ability 时间戳,不能只引用 Flutter 日志。

2. 首屏优化的验收表

验收项通过条件
首帧页面不白屏,基础控件可见
交互首个按钮在目标时间内可点击
延后加载有加载中和失败重试状态
生命周期页面退出后异步结果不再更新
内存延后任务完成后 PSS 回到预算范围

这张表比单独追求一个更小的 firstFrameMs 更接近真实上线标准。

五、复现命令和测试范围

flutter pub get
flutter analyze
flutter test
flutter build hap --debug --no-codesign
flutter run --profile -d <ohos-device-id>

启动后分别在延迟任务完成前、完成后保存页面截图,读取 STARTUP_BENCHMARK firstFrameMs=... dataItems=...,再重复多次。截图证明首屏和后续数据状态,不能单独证明整个应用的冷启动已经优化。

六、真机截图和实际结论

首帧截图用于确认基础页面已经出现,延后加载截图用于确认 24 条模拟数据最终进入页面。两张图不能直接代表系统从桌面点击图标到应用可交互的完整冷启动时间;这个指标仍要通过平台侧时间戳单独采集。

七、这次实验的实际结论

addPostFrameCallback 适合把非关键初始化移到 Flutter 页面首帧之后,但它不是延迟一切工作的开关。权限、账号和安全配置等首屏前置条件仍应按业务依赖完成;日志、非首屏图片、下一页数据和低优先级缓存才适合后移。

当前 Demo 的 firstFrameMs 只表示 Flutter 页面首帧回调,不等于系统冷启动。正式上线前应分别记录 Ability 启动、Flutter 首帧、首个可交互和关键内容完成四个时间点,并在冷启动、热启动、profile 和 release 条件下重复采样。

欢迎加入CPF-Flutter 鸿蒙社区:https://atomgit.com/CPF-Flutter

Logo

讨论HarmonyOS开发技术,专注于API与组件、DevEco Studio、测试、元服务和应用上架分发等。

更多推荐