第5篇|应用启动慢半拍:把初始化任务从首屏链路拆出去
第5篇应用启动慢半拍把初始化任务从首屏链路拆出去摘要鸿蒙应用启动慢很多时候不是页面写得复杂而是把所有初始化都塞进了首屏之前。配置、用户状态、远程开关、缓存预热、埋点准备每个任务单看都不大叠在一起就会让首页“慢半拍”。我的处理方式是把启动任务分成三层首屏必需、首屏可降级、首屏后补齐。实际项目里我见过一种很隐蔽的启动问题真机冷启动后启动图很快消失但首页要等一会儿才出现偶尔网络差一点首页还会显示半截旧数据。代码里没有明显死循环构建也没问题最后发现是AbilityStage和首页同时在等一批初始化任务。这篇文章会把启动链路拆成可落地的工程结构哪些任务必须挡在首屏前。哪些任务可以给默认值首屏后再补。如何写一个轻量的启动任务编排器。页面怎样订阅“能力是否可用”而不是等待所有任务结束。先划边界启动慢不一定是首页慢排查启动体验时不要一上来就改首页布局。启动慢通常分三类类型表现重点看哪里窗口慢启动图停留时间长UIAbility 和 WindowStage首屏慢空白或骨架停很久首页数据和同步任务补齐慢首页出现后局部内容延迟配置、远程开关、缓存如果不先分层就容易把所有问题都算到首页组件上。更稳的做法是先把启动任务列出来标记它们和首屏之间的关系。启动任务不要全写在 aboutToAppear首页aboutToAppear适合做页面自己的准备不适合承接全应用初始化。下面这种写法短期方便后期会拖慢首屏// pages/HomePage.etsaboutToAppear():void{this.loadUserProfile()this.loadRemoteConfig()this.prepareSearchIndex()this.reportAppOpen()this.loadHomeCards()}这些任务性质完全不同用户状态可能影响首屏展示远程配置可以降级搜索索引可以延后打开日志不应该阻塞页面。把它们写在一起页面就会变成启动调度中心后面谁加任务都会顺手往这里塞。用 StartupTask 描述任务属性我会先定义一个很小的任务模型把任务是否阻塞首屏写清楚// startup/StartupTask.etsexporttypeStartupPhasebefore_first_frame|after_first_frameexportinterfaceStartupTask{name:stringphase:StartupPhase required:booleanrun():Promisevoid}这个模型只保留三件事任务名、运行阶段、是否必需。它不追求复杂调度能力但足够把启动任务从页面里拿出来。required的意义是失败后是否影响首屏继续打开而不是任务重不重要。把首屏必需任务控制到最少一个比较健康的启动链路里首屏前任务应该很少。比如// startup/tasks.etsimport{StartupTask}from./StartupTaskexportconststartupTasks:StartupTask[][{name:hydrate_user_session,phase:before_first_frame,required:true,run:async(){awaitUserSessionStore.hydrate()}},{name:load_remote_config,phase:after_first_frame,required:false,run:async(){awaitRemoteConfigService.refresh()}},{name:prepare_search_index,phase:after_first_frame,required:false,run:async(){awaitSearchIndexService.prepare()}}]这里的判断标准很直接没有用户会话首页可能连登录态都判断不了所以放在首屏前远程配置和搜索索引可以先用默认能力放到首屏后补齐。这样首页不会被一串非关键任务拖住。编排器只做一件事按阶段运行任务启动编排器不要写成庞大的框架。对大多数项目来说按阶段运行、记录失败、继续执行后续任务就够了。// startup/StartupOrchestrator.etsimport{StartupTask,StartupPhase}from./StartupTaskimport{startupTasks}from./tasksexportclassStartupOrchestrator{staticasyncrunPhase(phase:StartupPhase):Promisevoid{consttasksstartupTasks.filter((task)task.phasephase)for(consttaskoftasks){try{awaittask.run()console.info([startup]${task.name}finished)}catch(error){console.error([startup]${task.name}failed:${JSON.stringify(error)})if(task.required){throwerror}}}}}这段代码的边界是启动任务不关心具体业务。必需任务失败时抛出让入口进入兜底非必需任务失败时记录日志不挡住首屏。这样首屏体验和后台能力补齐就分开了。UIAbility 负责启动首屏不负责塞业务逻辑UIAbility的职责是创建窗口、加载首屏、安排启动阶段。不要把各种业务服务初始化直接写在里面。// entryability/EntryAbility.etsimport{StartupOrchestrator}from../startup/StartupOrchestratoronWindowStageCreate(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.etsEntryComponentstruct HomePage{StorageLink(remoteConfigReady)remoteConfigReady:booleanfalseStorageLink(searchReady)searchReady:booleanfalsebuild(){Column(){HomeHeader()CourseCardList()if(this.searchReady){SearchEntry()}else{SearchEntrySkeleton()}if(this.remoteConfigReady){OperationBanner()}}}}这个页面不再关心配置怎么加载只关心能力是否可用。首屏的核心列表先出来搜索和运营位按状态补齐用户感知会稳定很多。失败兜底要分“能启动”和“不能启动”启动失败不是一个状态。必需任务失败应用可能需要进入兜底页非必需任务失败只需要局部降级。失败位置处理方式用户会话水合失败进入登录或启动兜底页远程配置失败使用本地默认配置搜索索引准备失败隐藏搜索入口或展示骨架埋点初始化失败记录本地日志不影响页面区分这几类后启动链路会清楚很多。你不需要为了一个运营配置失败让整个首页都等在那里。我会怎样验证启动链路我通常按下面顺序复查清空应用数据后冷启动确认首屏能稳定出现。关闭网络后启动确认非必需任务不会挡住首页。模拟用户会话异常确认会进入兜底页。首屏出现后观察搜索、运营位是否能补齐。连续启动三次确认没有重复初始化和状态覆盖。这套顺序能覆盖首屏前、首屏后、异常状态和重复启动四个场景。常见问题和处理方式现象常见原因处理方式首页晚出来首屏前任务太多只保留用户会话等必需任务网络差时白屏远程配置阻塞首屏给默认配置首屏后刷新首页显示旧状态水合和刷新顺序混乱启动时先写安全默认值重进应用重复请求任务没有阶段和幂等控制编排器按阶段统一调度小结启动不是把任务跑完而是让首屏先可信启动优化的关键不是把每个任务都写得更快而是决定哪些任务真的要挡在首屏前。把启动任务从页面里拆出来按阶段编排页面只读能力状态失败时按影响范围兜底。这样应用启动会更可控后续新增任务也不会悄悄拖慢首页。