“启动慢”拆开看,大头在首帧之前

截图整理工具加了一个首页,展示最近导入的截图和分类概览。冷启动计时从点击图标到首页可交互,实测 2.8 秒。第一反应是系统慢,用 SmartPerf 拆开时间线,进程创建和系统初始化只占了 0.4 秒,剩下 2.4 秒里,首帧渲染和数据加载占了大头。

首页的 aboutToAppear 里做了三件事:同步读了两百条截图摘要、初始化了 OCR 和超分引擎、预加载了第一批缩略图。这三件事全部排在 build() 之前,首帧被硬生生推迟了。

首帧只画“壳”,数据异步补齐

aboutToAppear 的执行时机在 build() 之前。任何放在这里的同步 IO、大循环、第三方 SDK 首次初始化,都会原封不动加到首帧时间上。首帧只需要“能画出第一屏”的最小数据,其余全部后移。

typescript

@Entry
@ComponentV2
struct HomePage {
  @Local skeletonVisible: boolean = true;
  @Local recentShots: ScreenshotItem[] = [];

  aboutToAppear(): void {
    // 首帧只需要骨架屏,不读任何重数据
    setTimeout(async () => {
      this.recentShots = await loadRecentShots(20);
      this.skeletonVisible = false;
    }, 0);
  }

  build() {
    if (this.skeletonVisible) {
      HomeSkeleton()
    } else {
      RecentList({ data: this.recentShots })
    }
  }
}

setTimeout(..., 0) 在这里不是“延时”,是把任务从首帧关键路径上摘出去。骨架屏让用户感知到“正在出来”,而不是对着一片白猜应用是不是卡死了。首页两百条数据 ForEach 全量渲染的案例,只加载首屏十条、其余延迟拉取之后,首帧从 1.1 秒降到三百毫秒级别。

Application 初始化只保留首屏必需的

EntryAbility.onCreate 里串行初始化六个模块的写法,在开发阶段看不出来,真机冷启动直接多出半秒。网络请求首屏必需,留下;存储、埋点、推送、IM、地图按需延迟,用 setTimeout 或 TaskPool 分批启动。

typescript

async onCreate(want: Want): Promise<void> {
  await initNetwork();              // 首屏请求必需,保留
  setTimeout(async () => {
    await Promise.all([
      initStorage(),
      initAnalytics(),
      initPush()
    ]);
  }, 0);
  // OCR / 超分引擎按需,在用户首次使用对应功能时再初始化
}

不要把所有初始化都延迟——真正首帧需要的依赖必须同步完成,否则会出现白屏或跳变。延迟初始化也要做回归,避免引入“首次点击时才加载”的次生卡顿。

数据预加载看命中率,低于三成下沉为用时加载

首页的“最近截图”可以读缓存,这个读取是首帧必需的。但“全部分类统计”和“特征向量预热”这类数据,如果用户十次启动只有两次会点到,就不值得在冷启动时同步加载。判断标准是预加载命中率:如果某个预加载任务命中率低于 30%,或者让冷启动 P95 明显变差,就应该下沉为用时加载。

冷启动的优化不是把代码改乱,是把“首帧不需要的东西”和“首帧需要的东西”分开。Application 初始化拆成必需和延迟两批,首屏数据只取最小集,其余交给骨架屏之后异步补齐,再配合包体积和增量编译的构建侧优化,两秒半的启动压到一秒以内是工程上可达的目标。验收时看两个数字:首帧时间有没有降,延迟初始化有没有引入新的首次点击卡顿。两个都稳,优化才算成立。

Logo

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

更多推荐