在这里插入图片描述

📖 引言

想象一下这个场景:

你在地铁上,突然想查一下"傣族"的泼水节是什么时候。你点开「民族图鉴」,盯着屏幕等啊等……1秒过去了,2秒过去了,3秒过去了,App 还在显示启动页。地铁到站了,你无奈地关掉了 App。

启动速度,是用户对 App 的第一印象

这第一印象有多重要?根据 Google 的研究:

  • 53% 的移动端用户会放弃加载时间超过 3 秒的网站
  • 页面加载时间每增加 1 秒,转化率下降 7%
  • 79% 的用户表示,如果一个 App 速度慢,他们不会再用第二次

对于移动 App 来说,启动速度更是重中之重。用户点击图标,越快看到内容,体验就越好。反之,启动越慢,用户流失的概率就越高。

你可能会问:

  • 启动的过程到底发生了什么?为什么需要这么久?
  • 冷启动、温启动、热启动有什么区别?
  • 启动时间怎么测量?怎么知道慢在哪?
  • 启动优化有哪些手段?分别能提升多少?
  • 启动页(Splash)应该怎么设计?
  • 白屏、黑屏这些问题怎么解决?

这些问题非常关键。启动优化是性能优化中性价比最高的方向之一——往往改几行代码,就能获得几百毫秒甚至几秒的提升,用户感知非常明显。

本文将带你深入理解启动性能优化的方方面面。我们会从启动的三种形态讲起,解析完整的启动流程,学习测量方法,然后逐一讲解启动优化的核心手段,最后结合「民族图鉴」项目进行实战优化。


🎯 学习目标

完成本文后,你将能够:

  • ✅ 理解冷启动、温启动、热启动的区别与联系
  • ✅ 掌握完整的启动流程与各阶段耗时分布
  • ✅ 学会启动时间的两种测量方法:打点测量与工具测量
  • ✅ 掌握延迟初始化的设计与实现
  • ✅ 掌握异步初始化的使用场景与注意事项
  • ✅ 理解启动页的设计原则与优化技巧
  • ✅ 能够独立完成「民族图鉴」的启动优化
  • ✅ 解决白屏、黑屏、启动动画不流畅等常见问题

💡 需求分析

启动为什么会慢?

在讲优化之前,我们先搞清楚一个问题:从点击图标到看到内容,中间到底发生了什么?为什么需要时间?

很多人以为启动慢是因为"手机卡"或者"App 大",但实际上,启动慢的原因可能有很多:

原因 说明 常见程度
系统调度慢 进程创建、资源分配需要时间 低(系统优化得很好)
初始化太多 Application/Ability 里做了太多事
首页太复杂 首屏布局层级深、组件多
数据加载慢 启动时加载大量数据
第三方 SDK 启动时初始化一堆 SDK
主线程阻塞 主线程做了耗时操作

对于「民族图鉴」这样的应用,启动慢的主要原因通常是:

  1. 启动页固定等待 2.2 秒(不管数据加载完没)
  2. 多个服务同步初始化
  3. 首页布局虽然不算特别复杂,但还有优化空间

💡 想一想:你现在的项目,启动需要多久?慢在哪?不妨读完本文后,测一测自己的 App。


启动优化的价值

为什么要把启动优化单独拿一篇来讲?因为它的性价比极高

启动优化的特点

特点 说明
用户感知强 每次打开 App 都能感受到
优化空间大 很多 App 的启动都有很大优化空间
改造成本低 往往改几行代码就能有明显提升
业务价值高 直接影响留存率、转化率

根据经验,大部分 App 的启动时间都能优化 30%~50%,有些甚至能优化 70% 以上。投入不多,收益明显,非常值得做。


「民族图鉴」的启动现状

让我们先看看「民族图鉴」当前的启动实现:

EntryAbility.onCreate

  • 设置颜色模式
  • 打印日志

EntryAbility.onWindowStageCreate

  • 初始化 StorageService
  • 初始化 ThemeService(主题 + 内容模式)
  • 初始化 I18nService(语言设置)
  • 初始化 TTSEngineService(TTS 引擎)
  • 加载 SplashPage

SplashPage

  • 显示 Logo 动画
  • 固定等待 2.2 秒后跳转到首页

Index(首页)

  • 加载多个 Tab 页面
  • 初始化各种组件

预估启动时间:~2.5 秒(其中启动页就占了 2.2 秒)

这个启动时间说快不快,说慢也不算特别慢。但还有很大的优化空间——至少启动页的 2.2 秒固定等待就很不合理。

让我们开始系统地学习启动优化,然后把「民族图鉴」的启动速度提升一个档次。


🛠️ 核心实现

步骤1:理解启动的三种形态

在讲优化之前,我们先搞清楚启动的三种形态:冷启动温启动热启动

这三种形态的启动时间差异很大,优化手段也不一样。


1.1 冷启动(Cold Start)

定义:应用进程不存在,系统从头开始创建进程、初始化应用、加载页面。

典型场景

  • 第一次安装后打开
  • 杀掉进程后重新打开
  • 设备重启后第一次打开
  • 应用在后台被杀掉后重新打开

冷启动的流程最长

用户点击图标
    ↓
系统创建进程
    ↓
虚拟机初始化
    ↓
Application/Ability 初始化
    ↓
加载启动页
    ↓
首页渲染
    ↓
用户看到内容

冷启动时间最长,也是我们优化的重点。因为:

  • 用户感知最明显
  • 优化空间最大
  • 最能体现应用的性能水平

1.2 温启动(Warm Start)

定义:应用进程还在,但 Activity/Ability 被销毁了,需要重新创建页面。

典型场景

  • 按返回键退出应用后重新打开
  • 应用在后台停留了一段时间,系统回收了 Activity/Ability
  • 从其他 App 跳转回来,页面被重建了

温启动的流程

用户点击图标/切换回来
    ↓
Ability 初始化(进程已存在)
    ↓
加载启动页/首页
    ↓
用户看到内容

温启动比冷启动快,因为省去了进程创建和 Application 初始化的时间。一般只需要冷启动的 1/3 ~ 1/2 时间。


1.3 热启动(Hot Start)

定义:应用进程和 Activity/Ability 都在,直接从后台切到前台。

典型场景

  • 按 Home 键回到桌面,再切回来
  • 切换到其他 App,很快又切回来
  • 下拉通知栏,再收回去

热启动的流程最短

用户切回应用
    ↓
Activity/Ability 从后台到前台
    ↓
用户看到内容

热启动最快,一般几百毫秒就能完成。因为所有东西都在内存里,只需要从后台切到前台就行。


1.4 三种启动形态对比
维度 冷启动 温启动 热启动
进程 需要创建 已存在 已存在
Ability 需要创建 需要创建 已存在
启动时间 最长(1.5~5s) 中等(0.5~2s) 最短(< 0.5s)
优化空间 最大 中等 最小
用户感知 最强 中等 较弱

1.5 优化重点

我们应该把精力主要放在冷启动上,因为:

  1. 冷启动最慢,用户感知最强
  2. 冷启动的优化空间最大
  3. 冷启动优化了,温启动和热启动通常也会受益

但温启动和热启动也不能完全不管——毕竟用户大部分时候打开 App 都是温启动或热启动。

💡 经验法则:冷启动 < 1.5s,温启动 < 0.5s,热启动 < 0.3s,算是比较优秀的水平。


步骤2:启动流程解析

了解了三种启动形态,接下来我们深入解析冷启动的完整流程,看看每个阶段都在做什么、耗时多少。

冷启动可以分为两大阶段:系统阶段应用阶段


2.1 系统阶段(System Phase)

系统阶段是系统帮我们做的事情,这部分我们能优化的空间比较有限。

用户点击图标
    ↓
【1. 进程创建】
  • 分配进程资源
  • 创建虚拟机实例
  ↓
【2. 应用加载】
  • 加载应用代码和资源
  • 初始化 Application
  ↓
进入应用阶段

各阶段耗时(参考)

阶段 耗时(参考) 说明
进程创建 100~300ms 受设备性能影响
应用加载 200~500ms 受包体积影响
系统阶段合计 300~800ms 我们能做的不多

系统阶段的优化空间

  • 包体积优化(减少加载时间)
  • 资源优化(减少资源加载时间)
  • 代码优化(减少初始化时间)

这些优化的收益相对有限,所以我们重点还是放在应用阶段。


2.2 应用阶段(App Phase)

应用阶段是我们自己的代码在运行,这部分是优化的重点。

Application/Ability 初始化
    ↓
【1. 服务初始化】
  • 第三方 SDK 初始化
  • 基础服务初始化
  • 配置加载
  ↓
【2. 启动页加载】
  • 启动页布局渲染
  • 启动页动画
  • 数据预加载
  ↓
【3. 首页加载】
  • 首页布局渲染
  • 首页数据加载
  • 首帧渲染完成
  ↓
用户可交互

各阶段耗时(以「民族图鉴」为例)

阶段 当前预估 优化空间
服务初始化 ~300ms 可以延迟部分服务
启动页等待 ~2200ms 最大的优化空间!
首页渲染 ~200ms 有一定优化空间
应用阶段合计 ~2700ms 可以优化很多

看到了吗?启动页固定等待 2.2 秒,占了总启动时间的绝大部分。这是最不合理的地方,也是优化收益最大的地方。


2.3 启动时间的关键节点

为了准确测量和分析启动时间,我们需要定义几个关键的时间节点:

节点 说明 标记
T0 用户点击图标的时间 起点
T1 进程创建完成 系统阶段结束
T2 Ability.onCreate 开始 应用初始化开始
T3 Ability.onWindowStageCreate 开始 窗口创建
T4 启动页首帧渲染完成 用户看到启动页
T5 首页首帧渲染完成 用户看到首页内容
T6 首页数据加载完成、可交互 完全可用

常用的启动时间定义

指标 计算方式 说明
冷启动时间 T5 - T0 从点击到首页首帧
首屏时间 T6 - T0 从点击到内容完整可用
启动页显示时间 T4 - T0 从点击到看到启动页

不同团队可能有不同的定义,但核心都是"用户多久能看到内容"。

💡 建议:不仅要测量"首帧时间",更要关注"可交互时间"。有时候首帧出来了,但数据还没加载完,用户还是不能用,这样的"快"是假快。


步骤3:启动时间的测量方法

在优化之前,我们先要准确测量启动时间。没有测量,就没有优化。

测量启动时间有两种主要方法:打点测量工具测量


3.1 方法一:打点测量

打点测量就是在代码的关键位置加上时间戳,然后计算差值。

优点

  • 简单直接,想在哪打就在哪打
  • 可以精确到具体的函数/阶段
  • 不需要额外工具

缺点

  • 需要修改代码
  • 只能测自己加了点的地方
  • 系统阶段测不到

「民族图鉴」的打点实现

首先,在 EntryAbility 里加打点:

// entryability/EntryAbility.ets
import { hilog } from '@kit.PerformanceAnalysisKit';

const DOMAIN = 0x0000;
const TAG = 'StartupPerf';

export default class EntryAbility extends UIAbility {
  private startTime: number = 0;

  onCreate(want: Want, launchParam: AbilityConstant.LaunchParam): void {
    this.startTime = Date.now(); // T2: Ability 初始化开始
    hilog.info(DOMAIN, TAG, `onCreate start: ${this.startTime}`);
    
    // ... 原有逻辑
  }

  async onWindowStageCreate(windowStage: window.WindowStage): Promise<void> {
    const t3 = Date.now();
    hilog.info(DOMAIN, TAG, `onWindowStageCreate start, cost: ${t3 - this.startTime}ms`);
    
    // ... 原有逻辑
    
    // 记录服务初始化完成时间
    const tServices = Date.now();
    hilog.info(DOMAIN, TAG, `services init done, cost: ${tServices - this.startTime}ms`);
    
    // 加载页面
    windowStage.loadContent('pages/SplashPage', (err) => {
      const t4 = Date.now();
      hilog.info(DOMAIN, TAG, `SplashPage loaded, cost: ${t4 - this.startTime}ms`);
    });
  }
}

然后在 SplashPage 里加打点:

// pages/SplashPage.ets
@Entry
@Component
struct SplashPage {
  private pageStartTime: number = 0;

  aboutToAppear(): void {
    this.pageStartTime = Date.now();
    console.info('StartupPerf', `SplashPage aboutToAppear`);
    
    this.startAnimation();
    this.navigateToHome();
  }

  aboutToDisappear(): void {
    const duration = Date.now() - this.pageStartTime;
    console.info('StartupPerf', `SplashPage displayed for ${duration}ms`);
  }
  
  // ... 其他逻辑
}

最后在首页加打点:

// pages/Index.ets
@Entry
@Component
struct Index {
  @State isReady: boolean = false;

  aboutToAppear(): void {
    console.info('StartupPerf', `Index aboutToAppear`);
    
    // 模拟数据加载
    this.loadData().then(() => {
      this.isReady = true;
      console.info('StartupPerf', `Index data loaded, ready to use`);
    });
  }
  
  onPageShow(): void {
    console.info('StartupPerf', `Index onPageShow (first frame)`);
  }
  
  // ... 其他逻辑
}

查看日志
用 DevEco Studio 的 Log 面板,过滤 “StartupPerf” 标签,就能看到各个时间点的耗时。

💡 技巧:可以封装一个简单的性能打点工具类,方便统一管理和上报。


3.2 方法二:工具测量

除了手动打点,我们还可以用 DevEco Profiler 来测量启动时间。

优点

  • 不需要修改代码
  • 可以看到完整的调用栈
  • 可以测系统阶段的时间
  • 有图形化界面,直观

缺点

  • 需要连接设备
  • 配置稍复杂
  • 精确到函数级别,但打点更灵活

使用 DevEco Profiler 测启动时间的步骤

  1. 打开 DevEco Studio
  2. 连接设备或启动模拟器
  3. 打开 Profiler 面板(底部工具栏 → Profiler)
  4. 选择 “CPU” 或 “Time” 分析模式
  5. 点击 “Record” 开始录制
  6. 点击图标启动 App
  7. 等首页加载完成后,点击 “Stop” 停止录制
  8. 分析时间线,找到各个阶段的耗时

Profiler 能看到什么

  • 主线程的执行情况
  • 每个函数的调用耗时
  • 布局、绘制的时间
  • 垃圾回收(GC)的时间

3.3 两种方法对比
维度 打点测量 工具测量
精确度 高(自己控制打点位置) 中(受采样率影响)
易用性 需要改代码 开箱即用
系统阶段 测不到 可以看到
函数级分析 不行 可以
适合场景 日常开发、阶段耗时 深度分析、找瓶颈

建议

  • 日常开发用打点测量,快速看各阶段耗时
  • 遇到性能瓶颈用工具分析,深入定位问题
  • 两种方法结合使用,效果最好

3.4 测量注意事项

测量启动时间的时候,有几个注意事项:

  1. 多次测量取平均值
    每次启动的时间可能不一样,多测几次取平均,结果更准确。

  2. 杀掉进程后再测冷启动
    要测冷启动,一定要先把进程杀掉,不然可能是温启动。

  3. 在同一台设备上测
    不同设备性能差异很大,对比数据要在同一台设备上测。

  4. 关闭其他应用
    其他应用占用资源可能影响测量结果。

  5. Release 包测
    Debug 包有调试信息,性能比 Release 包差。最终数据要用 Release 包测。

💡 经验法则:至少测 5 次,去掉最高最低,取平均。这样数据比较可靠。


步骤4:启动优化的核心方向

了解了启动流程和测量方法,接下来我们讲启动优化的四大核心方向:

  1. 布局优化:让页面渲染更快
  2. 初始化优化:减少启动时的初始化工作
  3. 资源优化:减少资源加载时间
  4. 预加载:提前加载用户即将需要的内容

每个方向又有很多具体的优化手段,我们逐一讲解。


4.1 方向一:布局优化

页面布局越简单,渲染越快。启动页和首页的布局是优化重点。

布局优化的手段

手段 说明 收益
减少布局层级 能一层解决的不要套两层
简化启动页 启动页尽量简单,快速显示
首页懒加载 首屏只显示核心内容,其他延后
用轻量组件 优先用简单组件,不用复杂组件

举个例子:简化启动页

「民族图鉴」当前的启动页有 Logo 缩放动画、标题淡入、副标题淡入、进度条动画,效果很炫,但渲染成本也高。

如果追求极致的启动速度,可以把启动页简化:

  • 只用一个 Logo + 文字
  • 减少动画数量
  • 用更简单的布局

当然,这个要权衡——启动页也是品牌展示的一部分,不能为了快把体验搞差了。

💡 建议:启动页的原则是「快 + 好看」,先保证快,再在快的基础上做好看。


4.2 方向二:初始化优化

这是最重要、性价比最高的优化方向。

很多应用的启动慢,就是因为在启动路径上做了太多初始化工作——一堆第三方 SDK、各种服务、各种配置,全部挤在 Application.onCreate 或 Ability.onCreate 里同步执行,不慢才怪。

初始化优化的两个核心手段

  1. 延迟初始化:非核心服务,延后初始化
  2. 异步初始化:能异步的,就不要同步

我们接下来详细讲解这两个手段。


步骤5:延迟初始化——非核心服务延后初始化

延迟初始化(Lazy Initialization) 的核心思想是:能不用就不初始化,等用的时候再初始化。


5.1 什么是延迟初始化?

想象一下:你有一个工具箱,里面有 10 把螺丝刀。你每次用工具箱,都要把 10 把螺丝刀全部拿出来摆好,再开始干活。这是不是很傻?

正常的做法是:用哪把拿哪把,不用的就放在箱子里。

延迟初始化就是这个道理。启动的时候,只初始化真正需要的服务,其他的等用到了再初始化。

哪些服务可以延迟初始化?

类型 举例 能不能延迟
必须立即用的 主题、语言、存储 ❌ 不能延迟
进特定页面才用的 TTS、音乐播放器、AI 服务 ✅ 可以延迟
后台服务 统计、推送、崩溃收集 ⚠️ 可以异步
第三方 SDK 地图、支付、分享 ✅ 可以延迟

「民族图鉴」的服务分类

服务 启动时需要? 优化方案
StorageService ✅ 是(要读设置) 保持同步
ThemeService ✅ 是(影响显示) 保持同步
I18nService ✅ 是(影响显示) 保持同步
TTSEngineService ❌ 进详情页才用 延迟初始化
MusicService ❌ 进音乐页才用 延迟初始化
AIService ❌ 进 AI 页才用 延迟初始化

看到了吗?4 个服务里有 1 个可以延迟。如果以后加更多服务,能延迟的就更多了。


5.2 怎么实现延迟初始化?

最常用的方式是单例模式 + 懒加载

// services/TTSEngineService.ets
export class TTSEngineService {
  private static instance: TTSEngineService | null = null;
  private isInitialized: boolean = false;

  // 私有构造函数,防止外部直接 new
  private constructor() {}

  // 获取单例,第一次调用时初始化
  static getInstance(): TTSEngineService {
    if (!TTSEngineService.instance) {
      TTSEngineService.instance = new TTSEngineService();
      // 注意:这里不调用 init,等真正用的时候再初始化
    }
    return TTSEngineService.instance;
  }

  // 初始化方法,调用具体功能前确保初始化完成
  async init(): Promise<void> {
    if (this.isInitialized) return;
    
    console.info('TTSEngineService', 'Initializing TTS engine...');
    
    // 模拟初始化耗时操作
    await new Promise(resolve => setTimeout(resolve, 100));
    
    this.isInitialized = true;
    console.info('TTSEngineService', 'TTS engine initialized');
  }

  // 具体功能:朗读
  async speak(text: string, options?: any): Promise<void> {
    // 使用前确保初始化完成
    await this.init();
    
    // ... 实际的朗读逻辑
  }

  // 其他方法也一样,调用前先确保初始化
  async stop(): Promise<void> {
    if (!this.isInitialized) return;
    // ... 停止逻辑
  }
}

关键点

  1. 构造函数私有,只能通过 getInstance() 获取实例
  2. getInstance() 只创建对象,不做耗时初始化
  3. 真正的初始化在 init() 里,调用具体功能前才执行
  4. init() 有幂等保护,多次调用只初始化一次

这样,TTSEngineService 的初始化就从"启动时"推迟到了"用户第一次点播放按钮时"。启动时省了几十到上百毫秒。


5.3 延迟初始化的注意事项

延迟初始化虽然好,但也有一些坑需要注意:

注意点一:线程安全

如果多个线程同时调用 getInstance(),会不会创建多个实例?

在 JS 单线程模型下,这个问题不存在。但如果是 Native 代码或者多线程场景,就要考虑加锁了。

注意点二:首次使用卡顿

延迟初始化把初始化开销从启动时转移到了使用时。如果初始化耗时很长,用户第一次用的时候可能会卡一下。

解决方案

  • 如果初始化耗时很短(< 50ms),用户感知不到,没问题
  • 如果初始化耗时较长,可以在启动后异步预初始化(我们后面会讲)
  • 如果初始化耗时很长,可以显示加载提示

注意点三:初始化失败怎么办?

启动时初始化失败,我们可以提前处理。延迟初始化后,失败发生在用户使用过程中,处理起来更麻烦。

解决方案

  • 做好错误处理和降级方案
  • 关键服务还是启动时初始化比较好
  • 非关键服务失败了给用户提示就行

💡 经验法则:非核心、不常用、初始化不太快的服务,适合延迟初始化。核心、常用、初始化很快的服务,直接启动时初始化就行,省得麻烦。


步骤6:异步初始化——能异步的不同步

异步初始化的核心思想是:不阻塞主线程的初始化,放到后台去做。


6.1 什么是异步初始化?

启动的时候,有一些初始化工作:

  • 必须做(不能延迟)
  • 但不需要阻塞主线程(不用等它完成才能显示页面)

这种情况就适合用异步初始化——启动时开始初始化,但不阻塞主线程,页面继续加载,初始化在后台慢慢做。

哪些适合异步初始化?

类型 举例 能不能异步
影响首屏显示的 主题、语言、布局 ❌ 不能异步
不影响首屏的 统计 SDK、推送 SDK ✅ 可以异步
数据预加载 缓存预热、预加载首页数据 ✅ 可以异步
图片预加载 提前加载常用图片 ✅ 可以异步

「民族图鉴」的分析

服务 影响首屏? 优化方案
StorageService ✅ 是(要读主题/语言设置) 保持同步
ThemeService ✅ 是(影响显示) 保持同步
I18nService ✅ 是(影响显示) 保持同步
TTSEngineService ❌ 不是 延迟 + 可选异步预加载
数据预加载 ❌ 不是(首屏用 Mock 数据) 异步预加载

6.2 怎么实现异步初始化?

最简单的方式:用 setTimeout 或者 Promise 把初始化包起来。

// entryability/EntryAbility.ets
export default class EntryAbility extends UIAbility {
  async onWindowStageCreate(windowStage: window.WindowStage): Promise<void> {
    // ========== 必须同步初始化的 ==========
    const storage = StorageService.getInstance();
    await storage.init(this.context);
    
    const theme = ThemeService.getInstance();
    await theme.loadThemeSetting();
    await theme.loadContentModeSetting();
    
    const i18n = I18nService.getInstance();
    await i18n.loadLanguageSetting();

    // ========== 可以异步初始化的 ==========
    // 方式一:setTimeout
    setTimeout(() => {
      this.initAsyncServices();
    }, 0);

    // 加载启动页
    windowStage.loadContent('pages/SplashPage', (err) => {
      // ...
    });
  }

  private async initAsyncServices(): Promise<void> {
    try {
      // TTS 引擎预初始化(进详情页之前就准备好)
      const tts = TTSEngineService.getInstance();
      await tts.init();
      
      console.info('EntryAbility', 'Async services initialized');
    } catch (e) {
      console.error('EntryAbility', 'Async init failed:', JSON.stringify(e));
    }
  }
}

关键点

  1. 同步初始化必须的服务(主题、语言、存储)
  2. 非必须的用 setTimeout 异步初始化
  3. 异步初始化的失败不影响启动(失败了打个日志就行)

这样,启动的时候不用等 TTS 初始化完成,启动页可以更快显示。TTS 初始化在后台做,等用户进到详情页的时候,大概率已经初始化好了。


6.3 异步初始化的注意事项

注意点一:不要在异步初始化里操作 UI

异步任务在后台线程或下一个事件循环执行,这时候 UI 可能还没准备好。操作 UI 一定要在主线程。

在 HarmonyOS 的 ArkTS 里,默认都是在主线程执行的,所以这个问题不大。但如果用了 TaskPool 跑后台任务,就要注意了。

注意点二:初始化时序问题

如果某个功能依赖异步初始化的结果,而用户在初始化完成前就用了这个功能,会不会有问题?

解决方案

  • 功能内部做保护(比如前面 TTS 的 init 幂等)
  • 如果初始化还没完成,等它完成再执行
  • 或者显示加载状态

注意点三:不是越多越好

不要把所有初始化都丢到异步里。有些东西还是同步初始化更可靠:

  • 崩溃监控(越早越好)
  • 必要的配置读取
  • 影响首屏显示的

💡 经验法则:不阻塞、不影响首屏、失败了也没关系的初始化,可以异步。其他的还是老老实实同步吧。


步骤7:启动页优化——精简、预加载、占位

启动页(Splash Page / Launch Screen)是启动过程中用户看到的第一个界面。它的作用是:

  1. 品牌展示
  2. 缓解用户等待的焦虑
  3. 后台加载数据

启动页设计得好,用户会觉得"启动很快";设计得不好,用户会觉得"怎么这么慢"。


7.1 启动页的常见误区

先看看几个常见的错误做法:

误区一:固定等待 N 秒

// ❌ 不好:不管数据加载完没,都等 2 秒
aboutToAppear(): void {
  setTimeout(() => {
    this.navigateToHome();
  }, 2000);  // 固定 2 秒
}

这是最常见的误区!「民族图鉴」目前就是这么做的——固定等 2.2 秒。

为什么不好?

  • 如果数据 200ms 就加载完了,用户还要白等 1.8 秒
  • 如果数据加载要 3 秒,2 秒后跳过去,用户看到的是白屏的首页

误区二:启动页太复杂

启动页搞了一堆复杂的动画、视频,结果启动页本身加载就要 1 秒,还耗电。

误区三:启动页什么都不做

启动页只是显示个 Logo,后台什么都不干。等跳转到首页了,才开始加载数据。这不是浪费时间吗?


7.2 好的启动页应该怎么做?

好的启动页 = 快速显示 + 并行加载 + 智能跳转

原则一:启动页要快

  • 布局尽量简单
  • 图片尽量小
  • 动画适量,不要太复杂

原则二:启动页的时间 = 数据加载时间

  • 数据加载完了就跳转,不要傻等
  • 如果加载太快(< 500ms),可以稍微等一下,防止闪一下就走

原则三:启动页期间后台并行加载

  • 预加载首页数据
  • 预初始化服务
  • 提前做准备工作

7.3 「民族图鉴」启动页优化实战

让我们来优化一下「民族图鉴」的启动页。

优化前

  • 固定等待 2.2 秒
  • 后台什么都不做

优化后

  • 并行加载数据(虽然现在是 Mock 数据,但以后可以扩展)
  • 数据加载完就跳转
  • 最少显示 800ms(防止闪屏)
// pages/SplashPage.ets
import router from '@ohos.router';

@Entry
@Component
struct SplashPage {
  @State logoScale: number = 0.5;
  @State logoOpacity: number = 0;
  @State titleOpacity: number = 0;
  @State subtitleOpacity: number = 0;
  @State progressWidth: number = 0;
  
  private startTime: number = 0;
  private readonly MIN_DISPLAY_TIME = 800; // 最少显示 800ms

  aboutToAppear(): void {
    this.startTime = Date.now();
    this.startAnimation();
    
    // 启动页显示期间,并行加载数据/初始化服务
    this.preloadData().then(() => {
      this.navigateWhenReady();
    });
  }

  /**
   * 预加载数据/服务(并行执行,不阻塞启动页显示)
   */
  private async preloadData(): Promise<void> {
    try {
      // 可以在这里预加载:
      // 1. 首页数据
      // 2. 常用服务初始化
      // 3. 缓存预热
      
      // 模拟一些预加载工作(实际项目替换成真实逻辑)
      await new Promise(resolve => setTimeout(resolve, 200));
      
      console.info('SplashPage', 'Preload done');
    } catch (e) {
      console.error('SplashPage', 'Preload failed:', JSON.stringify(e));
      // 预加载失败不影响启动
    }
  }

  /**
   * 智能跳转:
   * - 如果加载很快(< 800ms),等够 800ms 再跳(防止闪屏)
   * - 如果加载比较慢,加载完立刻跳
   */
  private navigateWhenReady(): void {
    const elapsed = Date.now() - this.startTime;
    const remaining = Math.max(0, this.MIN_DISPLAY_TIME - elapsed);
    
    setTimeout(() => {
      router.replaceUrl({ url: 'pages/Index' }).catch((err: Error) => {
        console.error('SplashPage', 'Navigate failed:', JSON.stringify(err));
      });
    }, remaining);
  }

  // ... 动画和布局代码保持不变
}

优化效果

项目 优化前 优化后
启动页等待 固定 2200ms 最少 800ms,最长 ~1000ms
后台工作 什么都不做 并行预加载
总启动时间 ~2500ms ~1200ms
提升幅度 - ~50%!

看到了吗?只是把启动页从"固定等待"改成"智能跳转",启动时间就减少了一半!这就是为什么说启动优化性价比高。

💡 MIN_DISPLAY_TIME 设多少合适?

一般 500~1000ms 比较合适:

  • 太短(< 300ms):启动页闪一下就没了,用户可能没看清
  • 太长(> 1500ms):用户等得不耐烦
  • 800ms 左右是比较舒服的时间

7.4 启动页的其他优化技巧

技巧一:用系统启动画面替代自定义启动页

很多系统支持配置启动画面(Launch Screen / Splash Screen),在应用加载过程中显示。这比自己写启动页更快,因为系统在应用加载前就显示了。

HarmonyOS 也有类似的机制,可以通过配置启动页背景来实现更快的启动体验。

技巧二:占位图/骨架屏

如果首页数据加载比较慢,可以:

  • 启动页直接跳转到首页
  • 首页先显示骨架屏(占位)
  • 数据加载完再填充内容

这样用户会觉得"启动很快,内容慢慢出来",比"在启动页等半天"体验好。

技巧三:品牌展示与效率平衡

启动页是品牌展示的好机会,但不要为了品牌而牺牲速度。

好的做法

  • Logo 简洁清晰
  • 动画短而精致
  • 背景色和 App 主色调一致,视觉上更连贯

步骤8:实战——「民族图鉴」启动速度优化全流程

讲了这么多理论,让我们来一次完整的实战——从头到尾优化「民族图鉴」的启动速度。


8.1 第一步:测量基线

优化前先测一下当前的启动时间。

测量方法:打点 + 多次测量取平均

测量结果(5 次平均)

阶段 耗时 占比
系统阶段 ~400ms 16%
服务初始化 ~300ms 12%
启动页等待 ~2200ms 70%
首页渲染 ~100ms 2%
总计 ~3000ms 100%

💡 说明:这里的数字是模拟的,实际数据需要你在自己的设备上测量。

结论

  • 启动页等待占了 70%,是最大的瓶颈
  • 服务初始化占 12%,可以优化
  • 系统阶段和首页渲染占比较小

8.2 第二步:制定优化方案

根据测量结果,我们制定以下优化方案:

优化项 优化手段 预期收益 优先级
启动页等待 从固定 2.2s 改成智能跳转(最少 800ms) -1200ms P0
TTS 初始化 从启动时同步改成延迟初始化 -80ms P1
服务预加载 启动页期间异步预加载 TTS 体验更好 P1
首页优化 首屏只渲染核心 Tab,其他懒加载 -50ms P2

总预期收益:~1300ms,从 3000ms 降到 1700ms 左右。


8.3 第三步:实施优化

优化一:启动页智能跳转

参考前面第7.3节的代码,把启动页改成智能跳转模式。

优化二:TTS 延迟初始化

把 TTSEngineService 改成懒加载单例模式,启动时不初始化,第一次用的时候再初始化。

同时,在启动页期间异步预初始化,这样用户进详情页的时候 TTS 已经准备好了。

优化三:首页轻量化

首页有 5 个 Tab,但首屏只显示第一个。其他 Tab 可以懒加载——切换到的时候再渲染。

(这个我们在布局渲染优化那篇再详细讲)


8.4 第四步:验证效果

优化完了,重新测量:

测量结果(5 次平均)

阶段 优化前 优化后 提升
系统阶段 ~400ms ~400ms -
服务初始化 ~300ms ~220ms -80ms
启动页等待 ~2200ms ~800ms -1400ms
首页渲染 ~100ms ~80ms -20ms
总计 ~3000ms ~1500ms -1500ms(-50%)

效果:启动时间从 3 秒降到 1.5 秒,提升了 50%!用户感知非常明显。


8.5 第五步:固化成果

优化验证通过了,我们要把成果固化下来:

  1. 建立性能基线:把 1.5 秒作为冷启动的基线
  2. 加入监控:在打点的基础上,加入启动时间的统计上报
  3. 代码规范:把"新服务默认延迟初始化"写入规范
  4. 定期回归:每次大版本更新后测一下启动时间,防止退化

步骤9:常见问题与解决方案

最后,我们来聊聊启动优化中常见的问题和解决方案。


9.1 问题一:启动白屏

现象
点击图标后,屏幕白一下,然后才显示启动页或首页。

原因

  • 应用还在加载,窗口已经显示了,但内容还没渲染出来
  • 启动页背景色和系统窗口背景色不一致

解决方案

  1. 设置窗口背景
    给窗口设置一个和启动页一致的背景色,这样即使内容还没加载出来,用户看到的也是一致的颜色,不会觉得"白屏"。

  2. 用系统启动画面
    配置系统级的启动画面,应用加载过程中由系统显示,体验更连贯。

  3. 加快首帧渲染
    优化启动页布局,让首帧尽快出来。白屏的本质就是首帧太慢。


9.2 问题二:启动黑屏

现象
点击图标后,屏幕黑一下,然后才显示内容。

原因

  • 通常是主题设置的问题
  • 或者是启动时做了太耗时的操作,主线程卡住了

解决方案

  1. 检查主题配置
    确保主题不是黑色背景,或者和启动页一致。

  2. 排查主线程耗时操作
    用 Profiler 看一下启动时主线程在做什么,是不是卡住了。

  3. 异步初始化
    把耗时操作移到异步,不要阻塞主线程。


9.3 问题三:启动动画不流畅

现象
启动页的动画一顿一顿的,不流畅。

原因

  • 动画太复杂,每一帧要做的事情太多
  • 同时在做其他耗时操作(比如加载数据),主线程忙不过来
  • 图片太大,解码耗时

解决方案

  1. 简化动画
    减少动画元素的数量,用更简单的动画效果。

  2. 动画和加载错开
    不要让动画和重操作同时进行。可以先播动画,动画快完了再开始加载;或者先加载,加载完再播退场动画。

  3. 优化图片
    启动页的图片要小,用合适的尺寸和格式。

  4. 用硬件加速
    确保动画开启了硬件加速。


9.4 问题四:启动后首页数据加载慢

现象
启动页跳转到首页后,首页还是空白的,要等一会儿数据才出来。

原因

  • 首页数据量太大
  • 数据在主线程加载,阻塞渲染
  • 网络请求慢

解决方案

  1. 启动页预加载
    在启动页显示期间,就开始加载首页数据。等跳转到首页的时候,数据已经有了。

  2. 骨架屏/占位图
    首页先显示骨架屏,给用户一个"正在加载"的预期,比白屏体验好。

  3. 数据分页/懒加载
    不要一次性加载所有数据,先加载首屏的,其他的滚动到了再加载。

  4. 缓存
    用缓存数据先显示,然后后台刷新最新数据。


9.5 问题五:冷启动和热启动差别太大

现象
冷启动要 3 秒,热启动只要 200ms,差别太大,用户觉得不稳定。

原因
这是正常现象。冷启动要做的事情多,热启动要做的事情少,当然差别大。

解决方案

  1. 优化冷启动
    这是最根本的——冷启动快了,差别自然就小了。

  2. 热启动也加个过渡
    如果热启动太快,切回来的时候可以加个短的过渡动画,让体验更一致。(不过一般不需要,快是好事)

  3. 保持状态
    热启动时恢复用户之前的状态,让用户觉得"一直都在"。

💡 小提示:热启动快是好事,不用刻意放慢。用户不会因为"切回来太快"而不满的。


⚠️ 常见问题与解答

Q1:启动优化到多少算快?

A:取决于应用类型和设备。

参考标准(中高端设备)

应用类型 冷启动优秀 冷启动合格
工具类(如计算器) < 1s < 2s
内容类(如「民族图鉴」) < 1.5s < 3s
社交类 < 2s < 4s
游戏类 < 3s < 5s

对于「民族图鉴」这样的内容类应用,冷启动 < 1.5 秒算是优秀,< 2 秒是合格。


Q2:启动优化有优先级吗?先优化什么?

A:有优先级。按性价比从高到低排序:

  1. ⭐⭐⭐⭐⭐ 启动页优化(改几行代码,提升几百到几千毫秒)
  2. ⭐⭐⭐⭐ 延迟初始化(把非核心服务延后,改动小,收益大)
  3. ⭐⭐⭐⭐ 异步初始化(把能异步的异步,不阻塞主线程)
  4. ⭐⭐⭐ 首页懒加载(首屏只加载核心内容)
  5. ⭐⭐ 布局优化(减少层级、简化布局)
  6. ⭐ 系统级优化(包体积、资源优化等)

简单说:先搞启动页,再搞初始化,最后搞布局和资源。


Q3:延迟初始化会不会导致首次使用卡顿?

A:有可能,但可以解决。

情况一:初始化很快(< 50ms)
用户感知不到,完全没问题。

情况二:初始化中等(50~200ms)
轻微卡顿,大部分用户注意不到。可以接受,或者用异步预加载解决。

情况三:初始化很慢(> 200ms)
用户能明显感觉到卡顿。解决方案:

  • 启动后异步预初始化(启动页期间就开始初始化)
  • 显示加载状态(比如"正在准备中…")
  • 拆分初始化(先初始化必须的,其他的后面再初始化)

Q4:启动页要不要加广告?

A:从性能和体验的角度,不建议加。但从商业角度,可能需要加。

如果一定要加广告,注意几点

  1. 不要影响启动速度
    广告可以在启动页显示期间并行加载,不要等广告加载完才启动。

  2. 给用户跳过选项
    不要强制用户看完整广告。

  3. 控制时长
    广告不要太长,3~5 秒最多了。

  4. 缓存广告
    提前缓存好广告素材,不要每次都现下载。

💡 建议:小团队/小应用就别加启动页广告了,得不偿失。用户体验的损失远大于那点广告收入。


Q5:启动优化和包体积优化有关系吗?

A:有关系,但关系不是特别大。

包体积对启动的影响:

  • 代码体积:影响代码加载和验证的时间
  • 资源体积:影响资源加载的时间
  • 影响程度:一般包体积减少 10MB,启动时间快几十毫秒

所以包体积优化对启动有帮助,但不是最大的优化点。最大的优化点还是启动页和初始化。

当然,如果包体积特别大(几百 MB),那对启动的影响就比较明显了。


📝 本章小结

核心知识点

本文系统讲解了启动性能优化的完整知识:

1. 启动的三种形态

  • 冷启动:从头开始,最慢,优化重点
  • 温启动:进程在,页面重建,中等
  • 热启动:从后台切回,最快

2. 启动流程解析

  • 系统阶段:进程创建、应用加载(优化空间有限)
  • 应用阶段:服务初始化、启动页、首页加载(优化重点)

3. 启动时间测量

  • 打点测量:简单灵活,适合日常开发
  • 工具测量:Profiler,适合深度分析
  • 多次测量取平均,结果更可靠

4. 启动优化四大方向

  • 布局优化:减少层级,简化页面
  • 初始化优化:延迟初始化、异步初始化
  • 资源优化:减少包体积,优化资源
  • 预加载:提前加载,并行执行

5. 延迟初始化

  • 核心思想:用的时候再初始化
  • 实现方式:单例 + 懒加载
  • 注意事项:首次使用卡顿、初始化失败处理

6. 异步初始化

  • 核心思想:不阻塞主线程的放后台
  • 实现方式:setTimeout / Promise
  • 注意事项:时序问题、不要操作 UI

7. 启动页优化

  • 常见误区:固定等待、太复杂、什么都不做
  • 正确做法:快速显示 + 并行加载 + 智能跳转
  • 最少显示时间:500~1000ms,防止闪屏

8. 常见问题

  • 白屏/黑屏:窗口背景、主题配置、首帧优化
  • 动画不流畅:简化动画、错开加载、优化图片
  • 首页加载慢:预加载、骨架屏、分页加载

最佳实践总结

启动页不要固定等待,用智能跳转

// ✅ 好:数据加载完就跳,最少显示 800ms
private navigateWhenReady(): void {
  const elapsed = Date.now() - this.startTime;
  const remaining = Math.max(0, 800 - elapsed);
  setTimeout(() => this.navigateToHome(), remaining);
}

非核心服务用延迟初始化

// ✅ 好:第一次用的时候才初始化
static getInstance(): MyService {
  if (!MyService.instance) {
    MyService.instance = new MyService();
  }
  return MyService.instance;
}

async doSomething(): Promise<void> {
  await this.init(); // 使用前确保初始化
  // ...
}

能异步的初始化,不要同步

// ✅ 好:不阻塞主线程
setTimeout(() => {
  this.initAsyncServices();
}, 0);

启动页期间并行加载数据

// ✅ 好:一边显示启动页,一边加载数据
aboutToAppear(): void {
  this.startAnimation();
  this.preloadData().then(() => {
    this.navigateWhenReady();
  });
}

先测量,再优化

优化前先测基线,优化后再测结果。
用数据说话,不要凭感觉。

下一步预告

在下一篇文章(第63篇)中,我们将:

  • 🎨 深入理解渲染流程:布局 → 绘制 → 合成 → 提交
  • 🔍 搞懂什么是重排(Reflow)和重绘(Repaint)
  • ⚡ 学习减少重排重绘的各种技巧
  • 📐 掌握布局优化的最佳实践
  • 🔧 了解 renderGroup、离屏渲染、硬件加速
  • 🎯 实战优化「民族图鉴」详情页的布局性能
  • 💡 解决布局抖动、列表卡顿、首屏渲染慢等问题

布局和渲染性能直接影响界面的流畅度。搞懂渲染原理,掌握优化技巧,让你的 App 从"能用"变成"丝滑"。


🔗 相关链接


💡 提示:启动优化是性能优化中性价比最高的方向之一。很多时候,改几行代码(比如把启动页从固定等待改成智能跳转),就能获得几百毫秒甚至几秒的提升。先从最简单、收益最大的地方开始优化,一步步把启动速度提上去。记住:快,就是好。

Logo

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

更多推荐