16-首屏卡在启动页-把初始化任务分级放到可控链路 第16篇首屏卡在启动页把初始化任务分级放到可控链路摘要首屏卡住通常不是某个动画慢而是把太多初始化任务塞进了 AbilityStage、WindowStage 或启动页。要让首屏稳定先把启动任务分级首屏前只跑必需任务首屏后补缓存和诊断所有任务都能记录阶段和失败原因。我遇到过一个应用首次启动一直停在启动页日志里没有明显异常。后来发现启动页在等用户信息、课程缓存、远端配置和诊断上传全部完成。真正应该挡住首屏的只有本地状态水合和会话判断其余任务都可以后移。这篇文章会把问题拆成几个可落地的点给启动任务分级区分首屏前和首屏后。让 AbilityStage、WindowStage 和 SplashPage 各做自己的事。给每个初始化任务留下阶段标记和失败原因。用清空数据、慢网和首次启动路径验证首屏不再被非关键任务卡住。适用环境和边界本文讨论 Stage 模型下应用启动链路重点是 AbilityStage、UIAbility、WindowStage、启动页和初始化任务调度。示例不绑定具体业务适合课程、工具、内容类应用的首屏治理。维度建议边界首屏前本地状态水合、会话判断、必要路由准备首屏后缓存预热、远端配置、诊断上传、非关键统计页面职责启动页展示进度并进入首页不承载所有初始化验证方式首次安装、清空数据、慢网、离线和重启不展开内容不讨论复杂性能 profiling只处理启动任务分级启动链路最怕把所有任务都叫“初始化”。先分级才能知道哪些必须挡住首屏哪些应该延后。先把故障链路复盘出来首屏卡住时不要先改动画。先看是窗口没加载、启动页没跳转还是某个非关键任务一直没返回。阶段表面现象更深的原因应该看到的结果AbilityStage应用刚启动就停住同步任务过重或阻塞只做轻量水合WindowStage窗口创建后没有内容loadContent 失败或回调没记录可见页面先加载出来SplashPage一直留在启动页等待了缓存、网络和上传等非关键任务只等待 required 任务后置任务首页已进但继续卡顿后置任务和首屏抢资源分阶段调度并记录耗时把启动拆成阶段以后排查就能从“卡住了”变成“卡在 load-session 还是 warm-cache”。这比继续加 loading 文案有用。先定 owner再写代码启动 owner 要清楚。Ability 管生命周期启动页管可见过渡调度器管任务顺序业务服务只提供自己的初始化函数。对象归属不要越界做的事AbilityStage轻量水合和全局标记发网络请求或解析大文件WindowStage加载首个页面并记录失败等待业务缓存完成SplashPage等待 required 后跳首页吞掉所有后置任务StartupScheduler按阶段运行任务直接操作页面状态StartupReporter记录阶段和失败原因承载业务逻辑owner 清楚后启动优化就不是把代码从一个生命周期挪到另一个生命周期而是让任务在正确阶段执行。先列启动任务账本启动链路里这一步要回答一个问题它是否必须挡住首屏。如果不是就应该后移或单独记录。exportinterfaceStartupTask{name:stringlevel:required|afterFirstFrame|idlerun():Promisevoid}exportconstStartupTasks:StartupTask[][{name:hydrate-storage,level:required,run:hydrateStorage},{name:load-session,level:required,run:loadSession},{name:warm-course-cache,level:afterFirstFrame,run:warmCourseCache},{name:upload-diagnostics,level:idle,run:uploadDiagnostics}]启动任务先分级首屏前只放真正影响页面安全的任务。缓存预热和诊断上传不应该挡住首屏。AbilityStage 只做轻量准备启动链路里这一步要回答一个问题它是否必须挡住首屏。如果不是就应该后移或单独记录。exportclassAppAbilityStageextendsAbilityStage{onCreate():void{hydrateStorage()StartupReporter.mark(ability-stage-created)}}AbilityStage 适合做极轻量、必须的初始化。不要把网络请求、缓存预热和大文件解析塞在这里。WindowStage 先加载可用页面启动链路里这一步要回答一个问题它是否必须挡住首屏。如果不是就应该后移或单独记录。exportclassEntryAbilityextendsUIAbility{onWindowStageCreate(windowStage:window.WindowStage):void{windowStage.loadContent(pages/SplashPage,(err){if(err.code){StartupReporter.fail(load-content,${err.code})}})}}窗口创建阶段优先把可见页面加载出来。首屏能展示以后再让页面或调度器继续跑非关键任务。调度器按阶段执行启动链路里这一步要回答一个问题它是否必须挡住首屏。如果不是就应该后移或单独记录。exportclassStartupScheduler{asyncrun(level:StartupTask[level]):Promisevoid{consttasksStartupTasks.filter((item)item.levellevel)for(consttaskoftasks){awaittask.run()StartupReporter.mark(task.name)}}}串行执行便于定位卡在哪个任务。任务很多时可以再并发但先有账本和阶段比直接 Promise.all 更可靠。页面只等待必需状态启动链路里这一步要回答一个问题它是否必须挡住首屏。如果不是就应该后移或单独记录。EntryComponentstruct SplashPage{Stateready:booleanfalseprivatescheduler:StartupSchedulercreateStartupScheduler()asyncaboutToAppear():Promisevoid{awaitthis.scheduler.run(required)this.readytruethis.openHome()this.scheduler.run(afterFirstFrame)}}Splash 只等待 required 任务。进入首页后再补后置任务首屏就不会因为非关键初始化卡住。启动耗时要留下阶段标记启动链路里这一步要回答一个问题它是否必须挡住首屏。如果不是就应该后移或单独记录。exportclassStartupReporter{privatestaticmarks:string[][]staticmark(name:string):void{this.marks.push(${Date.now()}:${name})}staticfail(name:string,reason:string):void{this.marks.push(${Date.now()}:${name}:${reason})}}启动问题最怕只知道卡住不知道卡在哪。阶段标记能帮助定位是水合、会话、页面加载还是后置任务。用反向搜索把旧路径清掉启动问题改完后重点找阻塞任务、同步大计算和散落的启动入口。rgAbilityStage|onWindowStageCreate|loadContent|SplashPageentry features common rgPromise.all|await .*Cache|warm|upload|diagnostics|setTimeoutentry features common rgsetOrCreate|AppStorage|StartupTask|StartupSchedulerentry features common搜索不是为了删掉所有 await而是确认每个 await 是否属于首屏前必需任务。验证路径要覆盖真实操作启动链路必须按首次安装路径验证。清空应用数据后启动确认首屏能进入首页。断网启动确认非关键网络任务不会卡住首屏。模拟缓存为空确认后置预热失败不影响主页面。查看阶段标记确认能定位每个 required 任务。重启应用确认不会重复执行昂贵任务。执行hvigorw assembleHap --no-daemon确认示例改法没有破坏构建。这组验证不要只在一个入口上跑。很多问题在主流程里看不出来切换页面、返回、清空数据、改主题、断网、换宽屏后才会暴露。风险和回退方案风险触发场景处理方式把任务全后移首屏缺少会话或默认值required 只保留安全必需项后置任务抢资源首页刚出现又卡顿分批执行并记录耗时失败被吞掉启动页无原因停住Reporter 记录失败阶段状态未水合StorageLink 首次读取 undefined启动前 setOrCreate 默认值回退方案的价值不是保守而是让一次修复能分阶段落地。先保住主路径再删除旧逻辑排查时才不会把多个变量混在一起。常见问题和处理方式现象常见原因处理方式启动页一直转等待了非关键网络任务拆成 required 和 afterFirstFrame首页进入后旧状态闪现默认值未水合AbilityStage 先 setOrCreate真机偶现卡住某个任务无超时或无标记给任务加阶段记录和失败返回关闭再开仍慢每次重复预热重任务按缓存版本决定是否执行这些问题看起来分散本质上都和 owner、状态来源、刷新时机和验证范围有关。只修一个按钮通常不够要让同一类路径都走回同一套规则。这次问题给我的复盘启动优化不是把代码藏到别的生命周期里而是把任务分级。首屏前保证安全默认值和会话判断首屏后再补缓存、配置和诊断。复盘这类问题时我会先把用户看到的现象落到工程 owner 上再看代码是不是沿着同一条边界走。只要 owner 不清后面就会变成谁顺手谁修owner 清楚以后验证也能回到具体路径而不是靠感觉判断。真正落到项目里我会用这份小清单收尾先把用户看到的现象对应到具体 owner避免只改表面 UI。再确认写入、读取、刷新、路由或资源是否沿着同一条链路。搜索旧入口、旧 key、硬编码值或临时输出能解释的保留解释不通的收口。用真实入口走一遍主路径再补空态、失败态、返回、重启或宽屏路径。把剩余风险写清楚下一次维护时就不用重新猜当时为什么这么改。小结首屏先可用再补齐能力启动优化不是把代码藏到别的生命周期里而是把任务分级。首屏前保证安全默认值和会话判断首屏后再补缓存、配置和诊断。 这类问题最终都要回到 owner、代码边界和真实路径验证不能只看某个片段是否写得顺眼。搜索旧入口、旧 key、硬编码值或临时输出能解释的保留解释不通的收口。4. 用真实入口走一遍主路径再补空态、失败态、返回、重启或宽屏路径。5. 把剩余风险写清楚下一次维护时就不用重新猜当时为什么这么改。小结首屏先可用再补齐能力启动优化不是把代码藏到别的生命周期里而是把任务分级。首屏前保证安全默认值和会话判断首屏后再补缓存、配置和诊断。 这类问题最终都要回到 owner、代码边界和真实路径验证不能只看某个片段是否写得顺眼。