HarmonyOS 冷启动优化实战:把首屏、初始化和懒加载拆清楚

请添加图片描述

很多 HarmonyOS 应用一开始并不慢,真正变慢通常发生在功能越堆越多之后:UIAbility.onCreate() 里加了账号初始化,onWindowStageCreate() 里加了数据库预热,首页 aboutToAppear() 又去读配置、请求接口、解析大 JSON。用户点击图标后,屏幕迟迟没有内容,开发者再去看代码,会发现每一段逻辑单独看都“有理由”,合在一起却把冷启动拖慢了。

这篇文章不讲泛泛的性能口号,只解决一个具体问题:HarmonyOS 应用冷启动阶段,如何把首屏展示、必要初始化、可延后任务拆开,并用 DevEco Profiler 复测闭环。

本文适合以下场景:

  1. 应用启动时白屏时间明显,首页内容加载慢。
  2. UIAbility、首页生命周期里混入了大量初始化逻辑。
  3. 不确定哪些任务必须启动前执行,哪些可以延后。
  4. 想用一套可迁移的 ArkTS 代码管理冷启动任务。
  5. 上架前希望把启动体验和性能风险收敛掉。

一、资料定位:这篇文章参考哪些官方能力

先把资料边界说清楚,避免读者照着做时找不到出处。

类型 资料/能力 用在本文哪里
官方最佳实践 应用冷启动时延优化 冷启动拆解、首屏优先、耗时任务治理
官方工具文档 DevEco Profiler 调优工具简介 性能录制、Trace 分析、瓶颈定位
官方分析模板 冷启动:Launch 分析 冷启动阶段复测和启动链路观察
官方性能实践 性能优化实践 避免生命周期执行高耗时操作、状态更新治理
官方案例 应用冷启动首帧完成时延问题分析 首帧问题分析思路

本文的代码以 HarmonyOS 5.0.0 及以上版本的 Stage 模型应用为边界,语言使用 ArkTS。不同 DevEco Studio 版本的 Profiler 面板名称可能略有差异,但分析路径是一致的:先记录基线,再拆任务,再复测首帧。

二、环境与版本边界

项目 建议配置
系统模型 Stage 模型
语言 ArkTS
API 边界 HarmonyOS 5.0.0+
IDE DevEco Studio,带 DevEco Profiler
目标页面 pages/HomePage
优化目标 冷启动首屏尽快出现,延后非首屏必要任务

本文不会把所有初始化都强行异步化。更稳妥的原则是:

任务类型 是否放在冷启动阻塞链路 示例
必须立即完成 可以,但要短 创建窗口、加载首页路由、读取极小本地启动配置
首屏可见后再做 不应阻塞首屏 用户画像、推荐配置、非首屏缓存
用户触发时再做 不应进入冷启动 二级页面数据、分享配置、复杂图表
CPU 密集型 不放主线程 大 JSON 解析、图片批处理、复杂加密计算

三、先看优化后的启动链路

请添加图片描述

实际项目里,我会把冷启动拆成五段:

  1. UIAbility.onCreate() 只做进程级轻初始化和日志起点。
  2. onWindowStageCreate() 只创建窗口和加载首页。
  3. 首页先渲染骨架屏或首屏必要内容。
  4. 首屏出现后,再启动非关键任务队列。
  5. 用 Profiler 和日志对比优化前后耗时。

这套拆法的核心不是“少写代码”,而是让每段代码有明确责任。冷启动最怕的是一堆看似合理的初始化散落在生命周期里,最后谁都不知道是哪一步拖慢首屏。

四、工程结构:把启动职责从页面里拿出来

请添加图片描述

推荐目录结构如下:

entry/src/main/ets/
├── entryability/
│   └── EntryAbility.ets
├── pages/
│   └── HomePage.ets
├── startup/
│   ├── StartupCoordinator.ets
│   ├── StartupLogger.ets
│   ├── StartupTask.ets
│   └── tasks/
│       ├── MinimalConfigTask.ets
│       ├── UserSessionTask.ets
│       └── DeferredCacheTask.ets
└── common/
    └── AppConstants.ets

这几个文件的职责不要混:

文件 职责 不应该做什么
EntryAbility.ets 创建窗口、加载首页、记录启动阶段 不写业务初始化细节
StartupCoordinator.ets 编排关键任务和延后任务 不直接操作 UI
StartupTask.ets 统一任务接口 不关心具体业务
HomePage.ets 首屏展示和触发首屏完成事件 不做大规模数据预热
DeferredCacheTask.ets 首屏后再执行缓存任务 不阻塞窗口创建

这种结构对后期排查很有帮助。冷启动慢时,先看 EntryAbilityStartupCoordinator,不用在几十个页面生命周期里翻来翻去。

五、配置入口:module.json5 只声明能力,不塞业务

module.json5 里只声明入口 Ability。不要试图通过配置文件承载业务初始化逻辑。

{
  "module": {
    "name": "entry",
    "type": "entry",
    "srcEntry": "./ets/Application/AbilityStage.ets",
    "abilities": [
      {
        "name": "EntryAbility",
        "srcEntry": "./ets/entryability/EntryAbility.ets",
        "description": "$string:EntryAbility_desc",
        "icon": "$media:app_icon",
        "label": "$string:app_name",
        "startWindowIcon": "$media:start_icon",
        "startWindowBackground": "$color:start_window_background",
        "exported": true,
        "skills": [
          {
            "entities": [
              "entity.system.home"
            ],
            "actions": [
              "action.system.home"
            ]
          }
        ]
      }
    ]
  }
}

这段配置的边界是:告诉系统哪个 Ability 是入口、启动窗口如何展示。它不解决业务初始化,也不应该承载性能优化逻辑。冷启动优化真正要处理的是入口代码执行路径,而不是把配置写得更复杂。

六、入口 Ability:窗口创建前不要做重活

下面是优化后的 EntryAbility.ets。重点看两处:onCreate() 只打点和初始化协调器,onWindowStageCreate() 尽快加载首页。

import { AbilityConstant, UIAbility, Want } from '@kit.AbilityKit';
import { window } from '@kit.ArkUI';
import { hilog } from '@kit.PerformanceAnalysisKit';
import { StartupCoordinator } from '../startup/StartupCoordinator';
import { StartupLogger } from '../startup/StartupLogger';

const DOMAIN = 0x0001;
const TAG = 'Startup';

export default class EntryAbility extends UIAbility {
  private coordinator?: StartupCoordinator;

  onCreate(want: Want, launchParam: AbilityConstant.LaunchParam): void {
    StartupLogger.mark('ability_on_create_start');

    this.coordinator = new StartupCoordinator(this.context);
    this.coordinator.prepareCriticalTasks();

    hilog.info(DOMAIN, TAG, 'EntryAbility created, launchReason=%{public}s',
      JSON.stringify(launchParam.launchReason));
    StartupLogger.mark('ability_on_create_end');
  }

  onWindowStageCreate(windowStage: window.WindowStage): void {
    StartupLogger.mark('window_stage_create_start');

    windowStage.loadContent('pages/HomePage', (err) => {
      if (err.code) {
        hilog.error(DOMAIN, TAG, 'load HomePage failed, code=%{public}d message=%{public}s',
          err.code, err.message);
        return;
      }

      StartupLogger.mark('home_page_load_content_done');
      this.coordinator?.runAfterHomeLoaded();
    });
  }

  onDestroy(): void {
    this.coordinator?.release();
    StartupLogger.mark('ability_destroy');
  }
}

代码解释:

  1. EntryAbility 的边界是系统入口和窗口加载,不负责业务细节。
  2. prepareCriticalTasks() 只准备必要任务,不直接执行一堆耗时逻辑。
  3. windowStage.loadContent() 成功后再进入下一阶段,避免首页未出现就预热大量缓存。
  4. onDestroy() 释放协调器,防止延后任务持有 Ability 上下文导致泄漏。

如果你现在的项目把数据库、网络、用户信息、推荐配置都写在 onCreate(),可以先不急着全部删除,先按“是否影响首屏”做分类,再迁移到协调器里。

七、启动任务接口:每个任务都要说明能不能阻塞首屏

为了避免任务越加越乱,可以先定义一个统一接口。

import common from '@ohos.app.ability.common';

export type StartupTaskPhase = 'critical' | 'deferred';

export interface StartupTask {
  name: string;
  phase: StartupTaskPhase;
  timeoutMs: number;
  run(context: common.UIAbilityContext): Promise<void>;
}

这个接口很小,但它强制每个启动任务回答三个问题:

  1. name:Profiler 和日志里能不能看懂任务名称。
  2. phase:它到底是关键任务,还是首屏后任务。
  3. timeoutMs:它如果异常卡住,最多允许影响多久。

不要小看 phase 字段。很多冷启动问题不是某个 API 慢,而是项目里没人给任务定级,所有逻辑都默认“启动时立刻执行”。

八、启动协调器:关键任务短,延后任务可控

StartupCoordinator 是本文的核心。它不关心页面细节,只负责把启动任务按阶段执行。

import common from '@ohos.app.ability.common';
import { hilog } from '@kit.PerformanceAnalysisKit';
import { StartupTask } from './StartupTask';
import { StartupLogger } from './StartupLogger';
import { MinimalConfigTask } from './tasks/MinimalConfigTask';
import { UserSessionTask } from './tasks/UserSessionTask';
import { DeferredCacheTask } from './tasks/DeferredCacheTask';

const DOMAIN = 0x0001;
const TAG = 'StartupCoordinator';

export class StartupCoordinator {
  private criticalTasks: StartupTask[] = [];
  private deferredTasks: StartupTask[] = [];
  private released: boolean = false;

  constructor(private context: common.UIAbilityContext) {}

  prepareCriticalTasks(): void {
    this.criticalTasks = [
      new MinimalConfigTask(),
      new UserSessionTask()
    ];

    this.deferredTasks = [
      new DeferredCacheTask()
    ];
  }

  async runCriticalBeforeHome(): Promise<void> {
    StartupLogger.mark('critical_tasks_start');

    for (const task of this.criticalTasks) {
      await this.runWithTimeout(task);
    }

    StartupLogger.mark('critical_tasks_end');
  }

  runAfterHomeLoaded(): void {
    setTimeout(() => {
      if (this.released) {
        return;
      }

      this.runDeferredTasks();
    }, 300);
  }

  release(): void {
    this.released = true;
  }

  private async runDeferredTasks(): Promise<void> {
    StartupLogger.mark('deferred_tasks_start');

    for (const task of this.deferredTasks) {
      await this.runWithTimeout(task);
    }

    StartupLogger.mark('deferred_tasks_end');
  }

  private async runWithTimeout(task: StartupTask): Promise<void> {
    const start = Date.now();

    try {
      await Promise.race([
        task.run(this.context),
        this.timeout(task)
      ]);

      hilog.info(DOMAIN, TAG, '%{public}s finished in %{public}d ms',
        task.name, Date.now() - start);
    } catch (error) {
      hilog.error(DOMAIN, TAG, '%{public}s failed: %{public}s',
        task.name, JSON.stringify(error));
    }
  }

  private timeout(task: StartupTask): Promise<void> {
    return new Promise((_, reject) => {
      setTimeout(() => {
        reject(new Error(`${task.name} timeout after ${task.timeoutMs}ms`));
      }, task.timeoutMs);
    });
  }
}

代码解释:

  1. criticalTasks 只放首屏必要任务,数量要少。
  2. deferredTasks 放首屏出现后再做的事情,例如缓存、预热、非首屏配置。
  3. runWithTimeout() 防止单个任务异常拖住整个启动链路。
  4. setTimeout(..., 300) 不是魔法数字,而是给首页渲染留出一点时间;实际项目要用 Profiler 复测后调整。

如果项目启动仍然慢,不要急着把所有任务并发执行。并发会降低总耗时,但也可能在冷启动阶段抢主线程、抢 IO,导致首屏更抖。先分类,再决定串行还是并行。

九、关键任务示例:只读取最小配置

MinimalConfigTask 只读取启动必须的轻量配置,例如是否展示新首页、是否启用灰度入口。不要在这里读大文件或请求接口。

import common from '@ohos.app.ability.common';
import { preferences } from '@kit.ArkData';
import { StartupTask } from '../StartupTask';

export class MinimalConfigTask implements StartupTask {
  name: string = 'MinimalConfigTask';
  phase: 'critical' = 'critical';
  timeoutMs: number = 120;

  async run(context: common.UIAbilityContext): Promise<void> {
    const store = await preferences.getPreferences(context, 'startup_config');
    const enableNewHome = await store.get('enable_new_home', false);

    AppStorage.setOrCreate<boolean>('enableNewHome', Boolean(enableNewHome));
  }
}

代码解释:

  1. 这个任务只信任本地轻量配置,不访问网络。
  2. 读取结果放入 AppStorage,首页可以快速拿到状态。
  3. timeoutMs 设置得很短,是为了提醒自己:关键任务不能无限膨胀。
  4. 如果配置读取失败,首页应使用默认值,不应该阻塞启动。

有些项目会在启动时拉远端配置再决定首页结构。这个做法风险很高,弱网下会直接拖慢冷启动。更稳的做法是:启动用上一次缓存的配置,远端配置首屏后更新。

十、用户状态任务:只判断登录态,不拉完整用户画像

登录态经常被写成冷启动重灾区。优化时要分清:启动阶段只需要知道“用户是否登录”,不一定需要完整昵称、头像、权益、推荐标签。

import common from '@ohos.app.ability.common';
import { preferences } from '@kit.ArkData';
import { StartupTask } from '../StartupTask';

export class UserSessionTask implements StartupTask {
  name: string = 'UserSessionTask';
  phase: 'critical' = 'critical';
  timeoutMs: number = 150;

  async run(context: common.UIAbilityContext): Promise<void> {
    const store = await preferences.getPreferences(context, 'user_session');
    const token = await store.get('access_token', '');

    AppStorage.setOrCreate<boolean>('hasLoginSession', String(token).length > 0);
  }
}

代码解释:

  1. 这段代码只判断本地是否存在登录凭据。
  2. 不在冷启动阶段请求用户详情接口。
  3. 首页可以先根据 hasLoginSession 展示“已登录骨架态”或“未登录入口”。
  4. 头像、昵称、会员权益等信息放到首屏后加载。

这就是冷启动优化里最常见的取舍:先让用户看到可交互界面,再逐步补齐非关键数据。

十一、首页代码:先渲染骨架,再触发延后任务

首页不要在 build() 里做计算,也不要在 aboutToAppear() 里塞一堆 await。下面的首页只做两件事:读取轻量状态、展示首屏内容。

@Entry
@Component
struct HomePage {
  @StorageLink('enableNewHome') enableNewHome: boolean = false;
  @StorageLink('hasLoginSession') hasLoginSession: boolean = false;
  @State private firstScreenReady: boolean = false;

  aboutToAppear(): void {
    this.firstScreenReady = true;
  }

  build() {
    Column({ space: 16 }) {
      Row() {
        Text('HarmonyOS 性能工作台')
          .fontSize(24)
          .fontWeight(FontWeight.Bold)

        Blank()

        Text(this.hasLoginSession ? '已登录' : '游客')
          .fontSize(14)
          .fontColor('#247A4D')
      }
      .width('100%')

      if (this.enableNewHome) {
        this.NewHomeCards()
      } else {
        this.StableHomeCards()
      }
    }
    .width('100%')
    .height('100%')
    .padding(20)
    .backgroundColor('#F6FAF8')
  }

  @Builder
  NewHomeCards() {
    Column({ space: 12 }) {
      Text('今日任务')
        .fontSize(18)
        .fontWeight(FontWeight.Medium)
      Text('首屏只展示必要内容,推荐、画像和缓存稍后加载。')
        .fontSize(14)
        .fontColor('#566')
    }
    .padding(16)
    .borderRadius(16)
    .backgroundColor(Color.White)
  }

  @Builder
  StableHomeCards() {
    Column({ space: 12 }) {
      Text('常用入口')
        .fontSize(18)
        .fontWeight(FontWeight.Medium)
      Text('使用稳定首页结构,避免启动时等待远端配置。')
        .fontSize(14)
        .fontColor('#566')
    }
    .padding(16)
    .borderRadius(16)
    .backgroundColor(Color.White)
  }
}

代码解释:

  1. 首页只读取 AppStorage 中已经准备好的轻量状态。
  2. build() 只描述 UI,不做 IO、不解析大数据。
  3. NewHomeCardsStableHomeCards 都是轻量组件,适合首屏展示。
  4. 如果首屏需要接口数据,建议先展示骨架,再在组件稳定后异步刷新。

如果首页是列表型页面,首屏可以先加载 8 到 12 条数据,不要启动时直接拉全量列表。列表性能优化可以单独写一篇,冷启动阶段只管首屏是否尽快可见。

十二、延后任务示例:缓存可以晚一点做

DeferredCacheTask 用于处理不影响首屏的缓存预热。比如推荐配置、二级页面缓存、搜索热词、非首屏图片索引等。

import common from '@ohos.app.ability.common';
import { preferences } from '@kit.ArkData';
import { StartupTask } from '../StartupTask';

export class DeferredCacheTask implements StartupTask {
  name: string = 'DeferredCacheTask';
  phase: 'deferred' = 'deferred';
  timeoutMs: number = 800;

  async run(context: common.UIAbilityContext): Promise<void> {
    const store = await preferences.getPreferences(context, 'runtime_cache');

    const now = new Date().getTime();
    await store.put('last_cache_warm_time', now);
    await store.flush();
  }
}

代码解释:

  1. 这个任务不影响首页第一屏,所以放在 deferred 阶段。
  2. flush() 可能产生 IO,不应该抢在窗口加载之前执行。
  3. 任务失败只影响缓存命中率,不应该影响用户进入应用。
  4. 如果延后任务很多,建议再按业务分组,避免首屏后瞬间打满资源。

实际项目可以进一步加“空闲时执行”策略,例如等用户进入首页后 1 到 2 秒再预热推荐模块。冷启动优化不是把任务藏起来,而是把它们安排在更合适的时间。

十三、打点工具:不用猜,用日志看启动阶段

Profiler 能看 Trace,但代码里也应该有清晰打点。下面这个工具用于记录阶段耗时。

import { hilog } from '@kit.PerformanceAnalysisKit';

const DOMAIN = 0x0001;
const TAG = 'StartupTrace';

export class StartupLogger {
  private static marks: Map<string, number> = new Map();

  static mark(name: string): void {
    const now = Date.now();
    StartupLogger.marks.set(name, now);
    hilog.info(DOMAIN, TAG, '%{public}s at %{public}d', name, now);
  }

  static cost(from: string, to: string): number {
    const start = StartupLogger.marks.get(from);
    const end = StartupLogger.marks.get(to);

    if (start === undefined || end === undefined) {
      return -1;
    }

    return end - start;
  }

  static printSummary(): void {
    const abilityToHome = StartupLogger.cost(
      'ability_on_create_start',
      'home_page_load_content_done'
    );

    hilog.info(DOMAIN, TAG, 'ability_to_home_cost=%{public}d ms', abilityToHome);
  }
}

代码解释:

  1. mark() 记录启动节点,方便和 Profiler 时间线互相印证。
  2. cost() 用来计算两个阶段之间的耗时。
  3. printSummary() 可以在首页加载完成后输出关键指标。
  4. 线上版本是否保留详细日志,要根据项目日志策略处理,避免泄露敏感信息。

建议至少保留这些节点:

打点名称 含义
ability_on_create_start Ability 创建开始
ability_on_create_end Ability 创建结束
window_stage_create_start 窗口阶段开始
home_page_load_content_done 首页内容加载完成
deferred_tasks_start 延后任务开始
deferred_tasks_end 延后任务结束

十四、用 Profiler 复测:不要只凭体感判断

优化冷启动时,至少做两轮测试:

阶段 操作 观察点
优化前 录制冷启动 Launch 白屏时间、首帧完成时延、主线程长任务
拆分后 再录制同样场景 loadContent 前后是否变短
延后任务后 观察首页后 1 到 3 秒 是否出现卡顿、掉帧、明显阻塞
弱网场景 网络慢或离线启动 首页是否仍能显示基础内容

推荐验证步骤:

1. 清理应用后台进程,确保是冷启动。
2. 打开 DevEco Profiler,选择 Launch/冷启动分析能力。
3. 点击桌面图标启动应用,录制启动过程。
4. 记录首帧完成时延、主线程长任务和关键方法耗时。
5. 应用优化代码后,重复同样设备、同样入口、同样网络条件。
6. 对比优化前后的启动阶段日志和 Profiler 时间线。

期望日志类似这样:

StartupTrace: ability_on_create_start at 1784120000123
StartupTrace: ability_on_create_end at 1784120000145
StartupTrace: window_stage_create_start at 1784120000151
StartupTrace: home_page_load_content_done at 1784120000392
StartupCoordinator: MinimalConfigTask finished in 18 ms
StartupCoordinator: UserSessionTask finished in 23 ms
StartupTrace: deferred_tasks_start at 1784120000710
StartupCoordinator: DeferredCacheTask finished in 64 ms

如果 home_page_load_content_done 已经很快,但用户仍感觉慢,就要继续看首页渲染和数据加载;如果 ability_on_create_startwindow_stage_create_start 很长,问题多半还在入口初始化。

十五、常见问题排查

现象 可能原因 排查方法 修复建议
点击图标后白屏久 onCreate()onWindowStageCreate() 有耗时任务 查看 Profiler Launch 时间线和启动日志 只保留窗口创建必要逻辑,业务初始化迁移到任务队列
首页出来后立刻卡一下 延后任务同时启动太多 观察首页后 1 到 3 秒主线程状态 延后任务分批执行,非关键任务继续后移
弱网启动很慢 冷启动等待远端配置或用户接口 断网启动测试 使用本地缓存兜底,网络数据首屏后刷新
登录用户启动慢 启动阶段拉完整用户画像 搜索首页前的用户接口调用 冷启动只判断 token,用户详情延后请求
优化后偶现空数据 首屏依赖被误放到延后任务 检查首页必需字段来源 把真正首屏必需数据放回关键任务,保持轻量
Profiler 录制结果波动大 测试条件不一致 对比设备、网络、后台状态 固定测试入口,至少录制 3 次取稳定结果

排查时不要一上来就改架构。先拿到启动阶段的证据:哪个回调慢、哪个任务慢、首屏前到底做了什么。冷启动优化最忌讳凭感觉删代码。

十六、发布前验收清单

在把优化合入主分支前,我会按下面清单检查:

检查项 是否通过
UIAbility.onCreate() 没有网络请求、数据库大查询、大 JSON 解析
onWindowStageCreate() 只负责窗口创建和首页加载
首页 build() 中没有 IO、复杂循环和大对象计算
首屏必要数据有本地兜底,不依赖弱网接口
非首屏任务已经进入 deferred 队列
每个启动任务有名称、阶段和超时时间
Profiler 已完成优化前后对比
弱网、离线、首次安装、升级后首次启动都测试过
日志不包含 token、手机号、用户隐私数据

这张表不是形式主义。很多应用上架前才发现启动体验不稳定,往往就是因为只测了正常网络、正常登录态,没有覆盖首次安装、弱网和缓存缺失。

十七、总结:冷启动优化不是把代码藏起来

HarmonyOS 冷启动优化的关键,不是把所有代码都异步化,也不是把任务随便丢到 setTimeout。真正稳定的做法是:

  1. 用 Profiler 和日志确认启动基线。
  2. 把入口 Ability 的职责收窄到窗口创建和首页加载。
  3. 把启动任务分成关键任务和延后任务。
  4. 首页先展示可交互内容,再补齐非关键数据。
  5. 用同样设备、同样入口、同样网络条件复测。

如果项目已经变慢,不要从“我要优化性能”这种大目标开始。先问一个更具体的问题:用户看到首屏之前,应用到底做了哪些事情?

把这条链路拆清楚,冷启动优化才会从玄学变成工程。

Logo

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

更多推荐