本文涉及 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 —— 自写:非关键服务的懒初始化封装exportclassLazyInit<T>{privatereadonlyfactory:()=>T;privateinstance:T|null=null;privatebuilt=false;constructor(factory:()=>T){this.factory=factory;}// 第一次访问才创建,且只创建一次get():T{if(!this.built){this.instance=this.factory();this.built=true;}returnthis.instance!;}// 首屏可交互后再预热,把重活从启动关键路径上摘掉warmUp():void{if(!this.built){this.get();}}}// 用法:埋点、风控这类非首屏必需服务,先登记不创建constreporter=newLazyInit(()=>createReporter());// 进入可交互后再 reporter.warmUp();2) 首屏占位骨架——结构固定的页面,先画占位再填数据:
// HomeSkeleton.ets —— 自写:首屏骨架最小实现@Componentexportstruct 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) 骨架与数据的切换——用一个状态位控制,首屏绝不出现纯白:
@Componentstruct HomePage{@Stateready:boolean=false;@Statepayload: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';exportclassPrefetchGate{privatestaticreadonlyTAG=0x0011;// 取安装预加载缓存;失败或空则回退本地缓存,再不行走实时请求asyncfetchHomePayload():Promise<string>{try{constdata=awaitcloudResPrefetch.getPrefetchResult(cloudResPrefetch.PrefetchMode.INSTALL_PREFETCH);if(data&&data.result){hilog.info(PrefetchGate.TAG,'hit install prefetch');returntypeofdata.result==='string'?data.result:JSON.stringify(data.result);}}catch(err){// 兜底第一层:预加载没命中——关键是不能在这里卡住首屏hilog.error(PrefetchGate.TAG,`prefetch miss code=${(errasBusinessError).code}`);}returnthis.readLocalCache();// 第二层兜底:应用自己的本地缓存}privatereadLocalCache():string{// PersistentStorage / 文件缓存,这里返回空串,调用方决定是否实时拉取return'';}}并在EntryAbility.onCreate里把首页缓存提前取出,首屏渲染直接消费:
// EntryAbility.etsimport{AbilityConstant,UIAbility,Want}from'@kit.AbilityKit';import{cloudResPrefetch}from'@kit.CloudFoundationKit';import{BusinessError}from'@kit.BasicServicesKit';exportdefaultclassEntryAbilityextendsUIAbility{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 名称、枚举取值、版本号、官方约束、体验阈值)来自以下官方文档;文中的结构、代码示例、决策流程与自检清单为本人整理编写:
- 应用冷启动时延优化(最佳实践)
- 预加载概述 - Cloud Foundation Kit
- 调用安装预加载 - Cloud Foundation Kit
- cloudResPrefetch(预加载模块 ArkTS API)
最后一句:启动优化的第一动作不是砍毫秒,是消灭白屏——先把"故障感"拉回"加载感",再谈把数字往下压。而消灭白屏最干脆的一招,是把货在用户点开之前就备到本地。