本文涉及 HarmonyOS 6.1 / Cloud Foundation Kit(5.0.3(15) 起预加载,6.1.0(23) 起跳链安装预加载)与冷启动时延优化的官方口径。文中的结构、代码示例、决策流程与自检清单为本人整理编写;未在真机逐行验证的部分,请以真机实测为准。

首开提速头图

引子:白屏不是"卡",是"还没货"

先说一句V哥自己的判断:首屏白屏,往往不是优化不够,是资源没预取。

很多团队一看到白屏就冲进去砍初始化、加懒加载,数字纹丝不动。原因很直接——白屏那一刻,首屏数据还在云上没出发,或者主线程卡在同步解析大 JSON 上,build 方法还没被调用。这类问题靠"优化"救不回来,得靠"提前把货备到本地"。

下面这条链路先记在脑子里:点图标 → 进程拉起 → onCreate → loadContent → 首帧 → 首屏数据就绪。白屏出现在"首帧之前"还是"首帧之后数据没到位",治法完全不同。


一、白屏的三种归因

V哥把它归成三类,每一类的画面感不一样:

归因画面感对症方案V哥的做法
资源没预取首帧画出来了,内容区还空着转圈预加载 + 本地缓存首页文本/图片提前落本地,首开直接读
主线程堵了点了图标"没反应",连骨架都出不来延迟初始化 + 子线程aboutToAppear 只留必需,重活丢 TaskPool
骨架没画一整片纯白,用户以为卡死占位骨架 Skeleton先画结构占位,数据到位再替换

白屏三种归因与对症方案

第二类最隐蔽:aboutToAppear 里写"拉配置 → 解析 → 加工 → 赋值"看起来顺手,开发机三百条假数据几毫秒就过;上线后三千条真实数据,同段代码性质全变——首帧被拖到全部计算之后。


二、按归因对药:三个自写封装

1) 延迟初始化非关键服务——把"用的时候才建"做成封装,启动链上只挂必需项:

// LazyInit.ets —— 自写:非关键服务的懒初始化封装
export class LazyInit<T> {
  private readonly factory: () => T;
  private instance: T | null = null;
  private built = false;

  constructor(factory: () => T) {
    this.factory = factory;
  }

  // 第一次访问才创建,且只创建一次
  get(): T {
    if (!this.built) {
      this.instance = this.factory();
      this.built = true;
    }
    return this.instance!;
  }

  // 首屏可交互后再预热,把重活从启动关键路径上摘掉
  warmUp(): void {
    if (!this.built) {
      this.get();
    }
  }
}

// 用法:埋点、风控这类非首屏必需服务,先登记不创建
const reporter = new LazyInit(() => createReporter());
// 进入可交互后再 reporter.warmUp();

2) 首屏占位骨架——结构固定的页面,先画占位再填数据:

// HomeSkeleton.ets —— 自写:首屏骨架最小实现
@Component
export struct HomeSkeleton {
  build() {
    Column({ space: 12 }) {
      // 顶部统计栏占位
      Row({ space: 12 }) {
        ForEach([0, 1, 2, 3], () => {
          Column({ space: 6 }) {
            Column().width(56).height(14).borderRadius(7).backgroundColor('#E6EEF7')
            Column().width(40).height(10).borderRadius(5).backgroundColor('#EEF3F9')
          }
        })
      }
      // 卡片流占位
      Column({ space: 10 }) {
        ForEach([0, 1, 2], () => {
          Column()
            .width('100%')
            .height(96)
            .borderRadius(12)
            .backgroundColor('#EDF2F9')
        })
      }
    }
    .padding(16)
  }
}

3) 骨架与数据的切换——用一个状态位控制,首屏绝不出现纯白:

@Component
struct HomePage {
  @State ready: boolean = false;
  @State payload: HomePayload | null = null;

  build() {
    if (!this.ready) {
      HomeSkeleton()        // 首帧先画骨架,体感是"加载中"而非"故障中"
    } else {
      HomeContent({ data: this.payload! })
    }
  }
}

三、冷启 / 热启 / 温启,要分开看

官方对三种启动给出了清晰定义(应用冷启动时延优化):

“冷启动是指当应用启动时,后台未存在该应用的进程,系统需为其创建新进程。完整的冷启动过程,指从用户点击桌面图标开始,至应用首页首帧渲染完成、数据完全展示。”
“热启动是指当应用已在后台运行且进程驻留在内存中时,用户再次打开应用,系统可直接从内存恢复应用状态,无需重新初始化加载资源。”

温启动则是进程还在,但主实例或页面已被销毁,只需重建实例或页面,速度介于两者之间。

V哥的判断:优化精力要压在冷启动。 它最复杂、最慢,也是白屏最高发的地方。热启基本是系统恢复内存状态,应用侧几乎无事可做;温启的重头戏是状态保存/恢复。所以后面所有手段,瞄准的都是"冷启首帧之前"那段。

冷启动三态与官方五段划分

官方把冷启动拆成 5 个阶段:进程创建&初始化、Application&Ability 初始化、Ability/AbilityStage 生命周期、加载绘制首页、网络数据二次刷新。前两段系统占比高,客户端改动收效小;后三段(尤其"加载绘制首页"和"网络二次刷新")才是我们能动刀的地方。


四、预加载能做什么:Cloud Foundation Kit

开头那句"资源没预取",官方给了现成能力——Cloud Foundation Kit 的预加载

官方口径(预加载概述):

“预加载是 Cloud Foundation Kit 提供的一种可提前加载所需资源的服务。通过预加载,可以将页面所需的文本、图片、音频、视频等资源数据提前加载到本地进行缓存,以提升应用页面加载速度。”

关键版本与类型(均为官方事实):

  • 起始版本:从 5.0.3(15) 起支持安装预加载、周期性预加载;从 6.1.0(23) 起支持跳链安装预加载;
  • 三种类型INSTALL_PREFETCH(安装预加载,首开提速)、PERIODIC_PREFETCH(周期性预加载,每 12h 拉一次,适合节日资源 / H5 离线包)、LINK_PREFETCH(跳链安装预加载,被分享用户首开详情页提速);
  • 配额:安装 2MB、周期 3MB、跳链 3MB;仅支持文本 / 图片 / 音频 / 视频等静态资源,不含代码脚本;
  • 设备:Phone、Tablet,6.1.0(23) 起新增 PC/2in1。

调用取数,官方 API 是 cloudResPrefetch.getPrefetchResult(Promise 或 callback 两种):

// PrefetchGate.ets —— 自写:预加载取数封装(含两层兜底)
import { cloudResPrefetch } from '@kit.CloudFoundationKit';
import { BusinessError } from '@kit.BasicServicesKit';
import { hilog } from '@kit.PerformanceAnalysisKit';

export class PrefetchGate {
  private static readonly TAG = 0x0011;

  // 取安装预加载缓存;失败或空则回退本地缓存,再不行走实时请求
  async fetchHomePayload(): Promise<string> {
    try {
      const data = await cloudResPrefetch.getPrefetchResult(
        cloudResPrefetch.PrefetchMode.INSTALL_PREFETCH
      );
      if (data && data.result) {
        hilog.info(PrefetchGate.TAG, 'hit install prefetch');
        return typeof data.result === 'string' ? data.result : JSON.stringify(data.result);
      }
    } catch (err) {
      // 兜底第一层:预加载没命中——关键是不能在这里卡住首屏
      hilog.error(PrefetchGate.TAG, `prefetch miss code=${(err as BusinessError).code}`);
    }
    return this.readLocalCache();   // 第二层兜底:应用自己的本地缓存
  }

  private readLocalCache(): string {
    // PersistentStorage / 文件缓存,这里返回空串,调用方决定是否实时拉取
    return '';
  }
}

并在 EntryAbility.onCreate 里把首页缓存提前取出,首屏渲染直接消费:

// EntryAbility.ets
import { AbilityConstant, UIAbility, Want } from '@kit.AbilityKit';
import { cloudResPrefetch } from '@kit.CloudFoundationKit';
import { BusinessError } from '@kit.BasicServicesKit';

export default class EntryAbility extends UIAbility {
  onCreate(want: Want, launchParam: AbilityConstant.LaunchParam): void {
    // 安装预加载:Ability 创建即取,命中就提前交给首屏
    cloudResPrefetch.getPrefetchResult(
      cloudResPrefetch.PrefetchMode.INSTALL_PREFETCH,
      (err: BusinessError, data) => {
        if (!err && data?.result) {
          GlobalContext.get().setObject('homeCache', data.result);
        }
        // 没命中也不阻塞,首屏照常用本地 / 实时数据
      }
    );
  }
}

“应用安装开始时,系统会拉取安装预加载云侧数据并缓存到本地。”官方还提醒:安装预加载缓存数据仅允许调用一次,被调用后将被销毁——所以取数要放在真正首开的地方,别在调试期随手调两次。


五、三个容易踩反的坑

坑一:预加载没做失败兜底,首开反而更慢。 这是本文的悬崖。预加载是"锦上添花"不是"雪中送炭"——它命中才提速,没命中(缓存失效、网络异常、配额超了)你若还傻等它的回调,首屏就被自己拖死。正确姿势:预加载走异步、不阻塞首帧,命中就用、没命中立刻切本地 / 实时。

坑二:开屏图不是优化。 开屏图盖在窗口最上层,把冻住的首页遮得严严实实;等开屏一撤,用户看到的是"早该出现但被堵在路上"的那一帧。很多人误以为加了开屏就是做了启动优化,其实只是把问题请到门外。官方反而建议把 startWindowIcon 分辨率控制在 256×256 以内,减少解码时延。

坑三:把首帧和可交互混为一谈。 两个指标要分开看:首帧是"用户看到第一个内容帧"(骨架屏也算,只要不是纯白);可交互是"首屏数据就绪、能点能滑"。白屏 1.5s 一次性全出,和 0.2s 骨架 + 0.8s 数据就位,总耗时差不多,体验是两个物种。


六、与本地缓存协同:预加载是"第一层",不是"唯一层"

预加载的缓存有配额(安装才 2MB)、有失效、有"仅调用一次"的限制。所以它不该孤军作战,要和应用自己的本地缓存叠成两层:

  • 第一层(云侧预取)cloudResPrefetch 安装 / 周期预加载,首开前把首页数据备到本地;
  • 第二层(端侧缓存):应用自身用 PersistentStorage 或文件缓存上一次成功数据,哪怕预加载 miss、网络也抖,首屏仍有"上次的货"撑着,不白屏。

两层都 miss,最后才落实时请求,且走骨架占位。这条链式兜底,才是"预加载提速但不拖慢"的真相。

预加载与本地缓存的两层协同


七、用工具量指标,别靠感觉

优化前先量,这是铁律。禁止凭"V哥觉得快了"下结论——启动耗时要用工具读。

  • DevEco Profiler → Launch 模板:录制冷启动,拆解各阶段耗时,火焰图看热点函数,Frame 泳道看首帧卡顿;
  • HiTrace(hiTraceMeter:在关键节点打 startTrace / finishTrace,把"onCreate → 首帧""网络二次刷新"的耗时钉出来;
  • AppAnalyzer → 手动性能冷启动体检:一键检测高耗时非 UI 操作、import 加载耗时、首页组件复杂度。

官方给的体验标尺:冷启动时延大于 1100ms 可认为启动缓慢;超过 3s 显著影响体验。这两个数字不是 KPI,是"该不该动手"的开关——先量到 1100ms 以上,再按本文的归因去对药。

本文不在真机跑任何实测,不会给出"从 2s 优化到 400ms"这类数字。启动耗时就是要用 Profiler 在真机量,不同设备、不同数据量差异巨大,编造数字既违规也误导。


八、上线自检清单

  • 白屏归因分清楚了吗:资源没预取 / 主线程堵了 / 还是骨架没画?
  • 首屏是否一定先画骨架(或占位),绝不出现纯白等待?
  • aboutToAppear 里还有没有同步解析大 JSON / 同步 IO?重活是否丢到 TaskPool / 子线程?
  • 非关键服务是否用 LazyInit 之类封装延迟到可交互后再初始化?
  • 预加载是否在 EntryAbility.onCreate 提前取数?命中是否直接喂首屏?
  • 预加载失败兜底做了吗?没命中是否立刻切本地缓存 / 实时请求,不阻塞首帧?
  • 安装预加载缓存是否只在"真正首开"取一次(避免调试期提前消耗)?
  • startWindowIcon 分辨率是否 ≤ 256×256?是否清理了 import * / export * 全量引用?
  • 长列表是否用 LazyForEach?首屏组件树嵌套是否压平?
  • 冷启动是否用 Profiler Launch 模板在真机量过?是否 > 1100ms 才动手优化?
  • 热启 / 温启的状态保存与恢复是否覆盖?是否会因页面销毁丢状态?

参考与出处

本文涉及的事实性信息(API 名称、枚举取值、版本号、官方约束、体验阈值)来自以下官方文档;文中的结构、代码示例、决策流程与自检清单为本人整理编写:


最后一句:启动优化的第一动作不是砍毫秒,是消灭白屏——先把"故障感"拉回"加载感",再谈把数字往下压。而消灭白屏最干脆的一招,是把货在用户点开之前就备到本地。

Logo

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

更多推荐