第5篇|应用启动慢半拍:把初始化任务从首屏链路拆出去

摘要:鸿蒙应用启动慢,很多时候不是页面写得复杂,而是把所有初始化都塞进了首屏之前。配置、用户状态、远程开关、缓存预热、埋点准备,每个任务单看都不大,叠在一起就会让首页“慢半拍”。我的处理方式是把启动任务分成三层:首屏必需、首屏可降级、首屏后补齐。

实际项目里,我见过一种很隐蔽的启动问题:真机冷启动后,启动图很快消失,但首页要等一会儿才出现;偶尔网络差一点,首页还会显示半截旧数据。代码里没有明显死循环,构建也没问题,最后发现是 AbilityStage 和首页同时在等一批初始化任务。
在这里插入图片描述

这篇文章会把启动链路拆成可落地的工程结构:

  1. 哪些任务必须挡在首屏前。
  2. 哪些任务可以给默认值,首屏后再补。
  3. 如何写一个轻量的启动任务编排器。
  4. 页面怎样订阅“能力是否可用”,而不是等待所有任务结束。

在这里插入图片描述

在这里插入图片描述

先划边界:启动慢不一定是首页慢

排查启动体验时,不要一上来就改首页布局。启动慢通常分三类:

类型 表现 重点看哪里
窗口慢 启动图停留时间长 UIAbility 和 WindowStage
首屏慢 空白或骨架停很久 首页数据和同步任务
补齐慢 首页出现后局部内容延迟 配置、远程开关、缓存

如果不先分层,就容易把所有问题都算到首页组件上。更稳的做法是先把启动任务列出来,标记它们和首屏之间的关系。

启动任务不要全写在 aboutToAppear

首页 aboutToAppear 适合做页面自己的准备,不适合承接全应用初始化。下面这种写法短期方便,后期会拖慢首屏:

// pages/HomePage.ets
aboutToAppear(): void {
  this.loadUserProfile()
  this.loadRemoteConfig()
  this.prepareSearchIndex()
  this.reportAppOpen()
  this.loadHomeCards()
}

这些任务性质完全不同:用户状态可能影响首屏展示,远程配置可以降级,搜索索引可以延后,打开日志不应该阻塞页面。把它们写在一起,页面就会变成启动调度中心,后面谁加任务都会顺手往这里塞。

用 StartupTask 描述任务属性

我会先定义一个很小的任务模型,把任务是否阻塞首屏写清楚:

// startup/StartupTask.ets
export type StartupPhase = 'before_first_frame' | 'after_first_frame'

export interface StartupTask {
  name: string
  phase: StartupPhase
  required: boolean
  run(): Promise<void>
}

这个模型只保留三件事:任务名、运行阶段、是否必需。它不追求复杂调度能力,但足够把启动任务从页面里拿出来。required 的意义是:失败后是否影响首屏继续打开,而不是任务重不重要。

把首屏必需任务控制到最少

一个比较健康的启动链路里,首屏前任务应该很少。比如:

// startup/tasks.ets
import { StartupTask } from './StartupTask'

export const startupTasks: StartupTask[] = [
  {
    name: 'hydrate_user_session',
    phase: 'before_first_frame',
    required: true,
    run: async () => {
      await UserSessionStore.hydrate()
    }
  },
  {
    name: 'load_remote_config',
    phase: 'after_first_frame',
    required: false,
    run: async () => {
      await RemoteConfigService.refresh()
    }
  },
  {
    name: 'prepare_search_index',
    phase: 'after_first_frame',
    required: false,
    run: async () => {
      await SearchIndexService.prepare()
    }
  }
]

这里的判断标准很直接:没有用户会话,首页可能连登录态都判断不了,所以放在首屏前;远程配置和搜索索引可以先用默认能力,放到首屏后补齐。这样首页不会被一串非关键任务拖住。

编排器只做一件事:按阶段运行任务

启动编排器不要写成庞大的框架。对大多数项目来说,按阶段运行、记录失败、继续执行后续任务就够了。

// startup/StartupOrchestrator.ets
import { StartupTask, StartupPhase } from './StartupTask'
import { startupTasks } from './tasks'

export class StartupOrchestrator {
  static async runPhase(phase: StartupPhase): Promise<void> {
    const tasks = startupTasks.filter((task) => task.phase === phase)

    for (const task of tasks) {
      try {
        await task.run()
        console.info(`[startup] ${task.name} finished`)
      } catch (error) {
        console.error(`[startup] ${task.name} failed: ${JSON.stringify(error)}`)
        if (task.required) {
          throw error
        }
      }
    }
  }
}

这段代码的边界是启动任务,不关心具体业务。必需任务失败时抛出,让入口进入兜底;非必需任务失败时记录日志,不挡住首屏。这样首屏体验和后台能力补齐就分开了。

UIAbility 负责启动首屏,不负责塞业务逻辑

UIAbility 的职责是创建窗口、加载首屏、安排启动阶段。不要把各种业务服务初始化直接写在里面。

// entryability/EntryAbility.ets
import { StartupOrchestrator } from '../startup/StartupOrchestrator'

onWindowStageCreate(windowStage: window.WindowStage): void {
  StartupOrchestrator.runPhase('before_first_frame')
    .then(() => {
      windowStage.loadContent('pages/HomePage')
      StartupOrchestrator.runPhase('after_first_frame')
    })
    .catch((error) => {
      console.error(`[startup] boot failed: ${JSON.stringify(error)}`)
      windowStage.loadContent('pages/StartupFallbackPage')
    })
}

这段链路把两件事说清楚:首屏前只跑必要任务;首屏加载后再跑补齐任务。失败也不是停在空白,而是进入明确的兜底页。

首页读取能力状态,不等待所有任务

首页不要等“所有初始化完成”才渲染。它可以先展示核心内容,再根据能力状态开启局部功能。

// pages/HomePage.ets
@Entry
@Component
struct HomePage {
  @StorageLink('remoteConfigReady') remoteConfigReady: boolean = false
  @StorageLink('searchReady') searchReady: boolean = false

  build() {
    Column() {
      HomeHeader()
      CourseCardList()

      if (this.searchReady) {
        SearchEntry()
      } else {
        SearchEntrySkeleton()
      }

      if (this.remoteConfigReady) {
        OperationBanner()
      }
    }
  }
}

这个页面不再关心配置怎么加载,只关心能力是否可用。首屏的核心列表先出来,搜索和运营位按状态补齐,用户感知会稳定很多。

失败兜底要分“能启动”和“不能启动”

启动失败不是一个状态。必需任务失败,应用可能需要进入兜底页;非必需任务失败,只需要局部降级。

失败位置 处理方式
用户会话水合失败 进入登录或启动兜底页
远程配置失败 使用本地默认配置
搜索索引准备失败 隐藏搜索入口或展示骨架
埋点初始化失败 记录本地日志,不影响页面

区分这几类后,启动链路会清楚很多。你不需要为了一个运营配置失败,让整个首页都等在那里。

我会怎样验证启动链路

我通常按下面顺序复查:

  1. 清空应用数据后冷启动,确认首屏能稳定出现。
  2. 关闭网络后启动,确认非必需任务不会挡住首页。
  3. 模拟用户会话异常,确认会进入兜底页。
  4. 首屏出现后观察搜索、运营位是否能补齐。
  5. 连续启动三次,确认没有重复初始化和状态覆盖。

这套顺序能覆盖首屏前、首屏后、异常状态和重复启动四个场景。

常见问题和处理方式

现象 常见原因 处理方式
首页晚出来 首屏前任务太多 只保留用户会话等必需任务
网络差时白屏 远程配置阻塞首屏 给默认配置,首屏后刷新
首页显示旧状态 水合和刷新顺序混乱 启动时先写安全默认值
重进应用重复请求 任务没有阶段和幂等控制 编排器按阶段统一调度

小结:启动不是把任务跑完,而是让首屏先可信

启动优化的关键不是把每个任务都写得更快,而是决定哪些任务真的要挡在首屏前。把启动任务从页面里拆出来,按阶段编排,页面只读能力状态,失败时按影响范围兜底。这样应用启动会更可控,后续新增任务也不会悄悄拖慢首页。

Logo

作为“人工智能6S店”的官方数字引擎,为AI开发者与企业提供一个覆盖软硬件全栈、一站式门户。

更多推荐