化核心:異步加載原理、落地方式與實戰(zhàn)指南)
1. 異步加載到底解決了什么問題做過前端或者客戶端性能優(yōu)化的朋友應(yīng)該都有過這種體驗頁面代碼越堆越多首屏打開越來越慢白屏?xí)r間從原來的500毫秒變成一秒兩秒用戶早跑了。剛開始我以為是網(wǎng)絡(luò)問題后來把Network面板打開一看一個首屏加載了2MB的JS、1MB的CSS還有一堆沒在首屏出現(xiàn)的圖片全擠在那條加載鏈里。問題很明確我們讓用戶為根本看不到的內(nèi)容付了費(fèi)——這個費(fèi)就是等待時間。異步加載的核心思想其實特別樸素現(xiàn)在不用的東西就別現(xiàn)在加載。但就是這句樸素的話落地到工程里卻牽扯出一整套設(shè)計和權(quán)衡。從圖片懶加載到路由級代碼分割從Web Worker到Android端的異步初始化背后共享的是同一個底層邏輯——把關(guān)鍵路徑上的任務(wù)砍短把非關(guān)鍵任務(wù)踢出主流程。這也是為什么性能優(yōu)化里異步加載幾乎是所有優(yōu)化手段的地基。你去做LCP優(yōu)化、做啟動耗時優(yōu)化、做TTI優(yōu)化最后大都會落到某些資源是不是可以晚點再加載這個決策上。這篇文章我打算把異步加載從原理到實操完整拆一遍包括它到底在優(yōu)化什么、有哪幾種落地方式、每種方式適合什么場景、踩過哪些坑以及怎么用數(shù)據(jù)驗證優(yōu)化到底有沒有效果。不管你是做Web前端、Android客戶端還是做跨端應(yīng)用這套思路都是通用的。我會把我在實際項目里驗證過的東西直接寫出來能幫你少走不少彎路。先說說異步加載之所以能提升性能的根本原因。瀏覽器和操作系統(tǒng)的渲染機(jī)制里有一條絕對的主干道——主線程。DOM的解析、樣式計算、布局、繪制、JavaScript的執(zhí)行全部擠在這條主干道上。同步加載的意思是一個任務(wù)沒走完后面所有任務(wù)全部排隊等著。你加載了一個同步腳本HTML解析器就得停下去拉腳本、編譯腳本、執(zhí)行腳本全部搞完才繼續(xù)解析后續(xù)標(biāo)簽。頁面內(nèi)容越靠后用戶看到首屏的時間就越晚。異步加載的本質(zhì)就是把不阻塞首屏渲染的任務(wù)挪到主干道之外或者挪到不那么緊急的時間點。這里有個非常重要的認(rèn)知異步加載不是減少工作量而是調(diào)整工作的時間分布。比如路由懶加載該寫的代碼一行沒少但寫代碼的那2MB文件從首屏必須下載并執(zhí)行變成了用戶進(jìn)入某個路由時才下載執(zhí)行。對于首屏來說它要處理的工作量就變少了所以首屏變快了。但對于整個應(yīng)用生命周期來說總工作量并沒有減少甚至可能因為分包拆得不合理還增加了請求次數(shù)。理解了這一層你就不會盲目地凡事皆異步而是會想清楚哪條路徑是用戶最關(guān)心的哪條路徑是可以往后放的。在做具體方案之前還需要構(gòu)建一個最基本的理論框架事件循環(huán)機(jī)制。JS是單線程的但通過事件循環(huán)把任務(wù)分成了同步任務(wù)、宏任務(wù)、微任務(wù)、渲染步驟等不同隊列。setTimeout和setInterval把任務(wù)延遲到宏任務(wù)隊列Promise.then、MutationObserver把回調(diào)排到微任務(wù)隊列requestAnimationFrame把任務(wù)排在渲染之前requestIdleCallback把任務(wù)排在瀏覽器空閑時段。這些API是異步加載的調(diào)度器你選擇哪個API實際上是在選擇任務(wù)在事件循環(huán)的哪個階段執(zhí)行這會直接影響用戶的感知流暢度。后面講具體場景時我會再回來說它們之間的取舍。2. 異步加載的五種主流落地方式異步加載不是某一種單一技術(shù)它是一族方案的統(tǒng)稱。我按實際項目的使用頻率把它們的實現(xiàn)原理、適用場景和踩坑點都列一下。理解這些方案的差異是做好選型的前提。2.1 資源級的異步加載defer與asyncHTML里加載外部腳本默認(rèn)是同步阻塞的。你寫了一個script srcapp.js放在head里瀏覽器的HTML解析器就會卡住下載并執(zhí)行完這個腳本才能繼續(xù)渲染。這其實是最古老的性能殺手。后來有了defer和async兩個屬性但它們的工作原理并不一樣。defer的意思是腳本的下載和HTML解析并行但執(zhí)行被推遲到HTML解析完畢之后。多個defer腳本會按照文檔順序依次執(zhí)行。async的意思是下載是異步的但一旦下載完成立刻中斷HTML解析去執(zhí)行腳本。多個async腳本誰先下載完誰先執(zhí)行跟文檔順序無關(guān)。從行為上看defer更適合那些依賴DOM結(jié)構(gòu)已經(jīng)準(zhǔn)備好的腳本async更適合那些獨(dú)立性強(qiáng)的腳本比如統(tǒng)計腳本、廣告腳本。這里有一個常見誤區(qū)很多人覺得在標(biāo)簽上加了async或者defer就萬事大吉其實不是。如果你有一個2MB的腳本加defer只是讓它執(zhí)行得晚一點下載仍然占了帶寬、也會在解析完畢后占用主線程執(zhí)行。所以資源級的異步加載只解決了不被阻塞解析的問題沒有解決加載了不該加載的東西的問題。代碼分割要配合路由懶加載去做資源級異步只是其中一塊拼圖。2.2 按需加載圖片懶加載與組件懶加載圖片懶加載是最常見也最容易上手的異步加載實踐。核心邏輯是圖片的真實地址不直接寫在src里而先放在>/** * 圖片懶加載工具類 * 用法new LazyLoad({ selector: .lazy-img }) */ class LazyLoad { constructor(options {}) { const defaultOptions { selector: .lazy-img, root: null, // 默認(rèn)使用瀏覽器視口作為觀察根元素 rootMargin: 0px 0px 200px 0px, // 提前200px開始加載提高加載感知流暢度 threshold: 0.01, // 元素進(jìn)入視口1%時觸發(fā)避免元素還在邊緣就加載 loadedClass: is-loaded }; this.options { ...defaultOptions, ...options }; // 保存所有未加載的圖片引用 this.images Array.from(document.querySelectorAll(this.options.selector)); this.init(); } init() { if (!(IntersectionObserver in window)) { // 降級方案直接加載所有圖片 this.images.forEach(img this.loadImage(img)); return; } this.observer new IntersectionObserver((entries) { entries.forEach(entry { // 只有isIntersecting為true時才真正觸發(fā)加載 if (entry.isIntersecting) { const img entry.target; this.loadImage(img); // 圖片一旦開始加載就不需要再被觀察了及時解除觀察 this.observer.unobserve(img); } }); }, { root: this.options.root, rootMargin: this.options.rootMargin, threshold: this.options.threshold }); this.images.forEach(img this.observer.observe(img)); } loadImage(img) { // 把data-src的真實地址還原到src同時兼容data-srcset響應(yīng)式圖片 const src img.getAttribute(data-src); if (src) { img.src src; img.removeAttribute(data-src); } const srcset img.getAttribute(data-srcset); if (srcset) { img.srcset srcset; img.removeAttribute(data-srcset); } // 加載完成后標(biāo)記狀態(tài)用于后續(xù)樣式控制 img.addEventListener(load, () { img.classList.add(this.options.loadedClass); }); // 加載失敗時嘗試用默認(rèn)占位圖兜底 img.addEventListener(error, () { img.src data:image/gif;base64,R0lGODlhAQABAAAAACH5BAEKAAEALAAAAAABAAEAAAICTAEAOw; img.classList.add(load-error); }); } } export default LazyLoad;幾個關(guān)鍵參數(shù)值得細(xì)說rootMargin我這里設(shè)置了0px 0px 200px 0px意思是在視口底部往下擴(kuò)展200px作為觸發(fā)區(qū)域。這樣用戶滾動到某張圖片前的200px時瀏覽器就已經(jīng)開始加載它了。等到圖片真正進(jìn)入視口大概率已經(jīng)加載完成用戶感知不到圖片突然出現(xiàn)的卡頓。這個200px不是一個固定值在網(wǎng)速較慢且圖片較多的場景可以調(diào)整為300px但也不能設(shè)太大——設(shè)置太大等于提前加載了大量還沒看到的圖片性能優(yōu)化就失真了。threshold我設(shè)為0.01元素哪怕只有1%進(jìn)入視口就觸發(fā)。如果設(shè)為0在某些瀏覽器里會有邊界情況——元素剛好在視口邊緣但沒有實際露出時可能不觸發(fā)所以用0.01來規(guī)避。unobserve一定要調(diào)用。圖片一旦開始加載就不需要繼續(xù)監(jiān)聽它的位置變化了不加這行的話觀察器會一直持有這些元素引用滾動過程中還會反復(fù)計算交叉狀態(tài)白消耗性能。4.3 避免布局抖動的占位方案與圖片渲染細(xì)節(jié)圖片懶加載最大的隱藏坑是布局抖動導(dǎo)致的CLS指標(biāo)惡化。普通圖片渲染有幾個階段HTML解析到img標(biāo)簽時只知道寬高屬性圖片資源加載完成后瀏覽器需要用實際尺寸重新布局。如果你沒有預(yù)先為圖片占位置加載完成后頁面高度突然增加下面的內(nèi)容全部往下跳用戶正在閱讀的位置就會被頂走——這個體驗非常糟糕。解決方案有兩個CSS預(yù)設(shè)寬高比和占位背景色。CSS預(yù)設(shè)寬高比現(xiàn)在最優(yōu)雅的實現(xiàn)是aspect-ratio屬性它讓元素在圖片加載前就占據(jù)和最終渲染尺寸接近的空間.lazy-img { width: 100%; aspect-ratio: 16 / 9; /* 圖片寬高比從設(shè)計稿或接口數(shù)據(jù)中獲取 */ object-fit: cover; /* 裁剪而不是拉伸保證視覺不扭曲 */ background-color: #f0f0f0; /* 加載前的占位底色弱化空白感 */ }用aspect-ratio的好處是高度在CSS計算階段就已經(jīng)確定瀏覽器不需要等圖片加載完成再做二次布局。不過前提是你得知道圖片的寬高比。如果是CMS后臺隨便上傳的圖片建議在服務(wù)端做一次圖片信息解析把寬高比下發(fā)給前端如果是固定尺寸的封面圖直接用固定比例就行。實在拿不到比例的情況就給一個min-height的保底值配合背景色兜底。還有一個細(xì)節(jié)是不要給懶加載圖片加過渡動畫。很多人喜歡給圖片加一個淡入效果opacity從0到1但opacity動畫本身會觸發(fā)合成層創(chuàng)建如果頁面上同時有幾十張圖片在滾動中淡入性能開銷反而大。content-visibility屬性其實是一個更好用的方案——它可以讓瀏覽器跳過屏幕外元素的渲染工作但兼容性目前還有限在團(tuán)隊技術(shù)棧允許的情況下可以作為補(bǔ)充手段。4.4 加載失敗兜底與網(wǎng)絡(luò)切換場景處理真實網(wǎng)絡(luò)環(huán)境比開發(fā)環(huán)境復(fù)雜得多。3G弱網(wǎng)、4G信號切換、Wi-Fi斷連這些都會導(dǎo)致圖片加載失敗。如果不做兜底頁面上會出現(xiàn)一片破圖用戶感知尤其差。我的兜底方案分了三層第一層是loading屬性給所有圖片加上loadinglazy作為瀏覽器的原生兜底。現(xiàn)代瀏覽器已經(jīng)原生支持懶加載即便沒有引入任何JavaScript代碼loadinglazy也能讓瀏覽器自動推遲屏幕外圖片的加載。但注意原生loadinglazy的觸發(fā)時機(jī)由瀏覽器決定開發(fā)者無法精細(xì)控制提前量也沒有失敗回調(diào)所以它適合做兜底不適合做主方案。第二層是上面代碼里的error回調(diào)圖片加載失敗時替換為一張極小體積的1x1透明GIF占位圖避免破圖圖標(biāo)顯示。同時給圖片加一個load-error類方便后續(xù)通過樣式表現(xiàn)加載失敗狀態(tài)。第三層是網(wǎng)絡(luò)狀態(tài)變化后的手動重試機(jī)制。監(jiān)聽navigator.onLine和online事件網(wǎng)絡(luò)恢復(fù)后重新掃描頁面上還有哪些>const LoginPage React.lazy(() import(./pages/Login)); const DashboardPage React.lazy(() import(./pages/Dashboard)); const UserManagePage React.lazy(() import(./pages/UserManage)); function RouterConfig() { return ( Suspense fallback{PageSkeleton /} Routes Route path/login element{LoginPage /} / Route path/dashboard element{DashboardPage /} / Route path/users element{UserManagePage /} / /Routes /Suspense ); }Vue Router則更簡單直接在路由配置里用動態(tài)import返回組件即可const routes [ { path: /dashboard, component: () import(./views/Dashboard.vue), meta: { title: 工作臺 } } ];代碼分割的核心收益不只是首屏體積變小還有一個隱藏收益是緩存命中率變高。業(yè)務(wù)代碼和公共庫代碼分開打包后公共庫的hash幾乎不變用戶第二次訪問其他頁面時公共庫直接命中緩存只需要下載業(yè)務(wù)chunk即可體驗提升非常明顯。5.2 移動端啟動性能優(yōu)化中的異步加載應(yīng)用異步加載在移動端的重要性比Web更突出因為移動端設(shè)備的主線程資源更緊張而且用戶對啟動速度的要求更高——從點擊圖標(biāo)到看到首幀超過兩秒用戶就會開始不耐煩。Android端啟動優(yōu)化的核心矛盾是Application的onCreate里有大量初始化任務(wù)SDK初始化、數(shù)據(jù)庫創(chuàng)建、緩存預(yù)加載、埋點模塊啟動這些任務(wù)如果全部同步執(zhí)行在onCreate里啟動耗時會被拉得很長。主流方案包括兩類第一類是啟動器框架如AndroidX StartUp。它的設(shè)計思想是把初始化任務(wù)抽象成一個一個有依賴關(guān)系的Task框架根據(jù)依賴關(guān)系構(gòu)建一個有向無環(huán)圖在沒有依賴關(guān)系的Task之間并行執(zhí)行有依賴關(guān)系的Task保持順序執(zhí)行。這本質(zhì)上就是一種異步加載——把原本串行、阻塞、全部擠在主線程的初始化流程改成了并行、按需、可延后的調(diào)度流程。第二類是更細(xì)力度的任務(wù)延后。那些非首屏必需的初始化比如推送SDK初始化、地理位置獲取、日志上傳可以放到首幀渲染完成之后再啟動。啟動耗時被首幀之前這個窗口嚴(yán)格約束首幀之前的任務(wù)越少啟動速度越快。任務(wù)可以細(xì)分為必須在主線程、可以在子線程、必須首幀前、可以首幀后四類逐一打標(biāo)簽后重新編排。這里有一個常見的坑子線程初始化看起來是異步了但如果多個子線程都去訪問同一個共享資源比如同一個SQLite數(shù)據(jù)庫或者都在做大量CPU計算線程之間的鎖競爭和CPU爭搶反而會讓主線程更卡。所以異步不意味著無腦開線程合理的做法是控制線程數(shù)盡量合并同類任務(wù)避免線程顛簸。5.3 前端框架中的異步組件與副作用處理在React和Vue之外跨端框架和小程序框架里也能做異步加載只是方式要跟著框架走。Taro/uni-app這類跨端框架頁面本身是按路由拆分的但頁面內(nèi)部仍可以通過動態(tài)import把模塊級的大依賴拆出去。比如頁面里有一個圖表組件用了ECharts這個圖表組件體積很大但只出現(xiàn)在頁面底部那就可以把它單獨(dú)拆出來等用戶滾動到底部時再加載。異步組件的加載過程里還有一個容易被忽視的副作用——狀態(tài)丟失。如果你在組件加載完成前用戶就已經(jīng)開始了某個交互操作等組件加載完成后這個交互狀態(tài)可能和組件的初始化狀態(tài)沖突。比如用戶在Modal打開前就點擊了確認(rèn)按鈕而承載確認(rèn)邏輯的異步組件還沒加載完點擊事件就丟了。所以異步組件場景下要做的事不只是加載組件還要處理加載期間的交互事件緩沖和重放。如果這個組件承載了關(guān)鍵業(yè)務(wù)流程我會傾向于把它的加載時機(jī)提前而不是極限延后——性能優(yōu)化不能以犧牲體驗一致性為代價。5.4 長列表的虛擬滾動與異步渲染結(jié)合長列表場景比如信息流、聊天記錄、商城商品列表一次性渲染幾百上千個DOM節(jié)點即使所有資源都本地已有渲染本身也會卡頓。虛擬滾動是長列表性能問題的標(biāo)配解法只渲染可視區(qū)域內(nèi)的元素其他區(qū)域留空占位。這和懶加載的邏輯一脈相承——都是不渲染、不加載當(dāng)前看不到的東西。虛擬滾動的實現(xiàn)邏輯比圖片懶加載復(fù)雜一些需要計算可視區(qū)域內(nèi)顯示哪些行根據(jù)滾動位置實時更新。成熟的庫有react-window、vue-virtual-scroller等也可以自己實現(xiàn)。自己實現(xiàn)的關(guān)鍵參數(shù)是預(yù)估行高——如果每條內(nèi)容的行高是固定的計算很簡單如果高度不固定比如文本長短不一就需要預(yù)估行高加上渲染后的實際高度校準(zhǔn)。校準(zhǔn)過程中上滑或下滑時的滾動條跳動是高頻問題一般通過給未渲染區(qū)域預(yù)設(shè)一個估算高度來規(guī)避。和異步渲染結(jié)合的方式是每條列表項內(nèi)部如果有圖片或按鈕等資源仍然可以配合懶加載或按需加載策略。列表項滾動進(jìn)入視口時才加載其內(nèi)部資源列表項滾出視口就銷毀或回收其DOM結(jié)構(gòu)。這樣疊加之后長列表才能做到萬條數(shù)據(jù)也不卡的流暢度。6. 異步加載的常見問題與性能排查實錄這部分是我在實際項目里踩過、也在團(tuán)隊里手把手帶人排查過的典型問題。按問題現(xiàn)象、排查思路、最終解決三列整理成速查表。6.1 高頻問題速查表問題現(xiàn)象可能原因解決方案懶加載圖片加載前出現(xiàn)布局跳動圖片容器未設(shè)置寬高比使用aspect-ratio或固定寬高預(yù)留占位懶加載后LCP指標(biāo)反而變差首屏LCP元素也被加了懶加載對LCP元素禁用懶加載改為立即加載異步組件加載后頁面白屏/報錯動態(tài)import的模塊加載失敗或路徑錯誤檢查打包輸出的chunk路徑配置webpackChunkName加ErrorBoundary滾動頁面卡頓、掉幀scroll事件監(jiān)聽回調(diào)里做了getBoundingClientRect或大量DOM操作改為IntersectionObserver如必須監(jiān)聽scroll用requestAnimationFrame節(jié)流大量異步請求同時發(fā)起導(dǎo)致接口排隊?wèi)屑虞d組件一次性全部觸發(fā)控制并發(fā)數(shù)重要資源優(yōu)先加載其他進(jìn)入延遲隊列使用原生loadinglazy后圖片始終不加載圖片在首屏或根元素設(shè)置了0高度檢查圖片父容器高度是否為0或改用IntersectionObserver方案Android啟動時子線程初始化導(dǎo)致主線程卡頓子線程任務(wù)過多且爭用共享資源統(tǒng)一用StartUp框架管理任務(wù)依賴與調(diào)度控制線程池數(shù)量異步加載了公共庫導(dǎo)致重復(fù)加載多個chunk都打包了同一份公共代碼配置依賴自動拆分splitChunks或manualChunks抽取公共依賴為單獨(dú)chunk6.2 疑難問題排查方法論與案例復(fù)盤這里挑一個最典型的案例復(fù)盤。我負(fù)責(zé)過的一個H5活動頁優(yōu)化前加載很流暢做完異步加載改造之后反倒變卡了。排查時先用Performance面板做了錄制發(fā)現(xiàn)主線程被一處占用了很久的任務(wù)給堵住了。追下去才發(fā)現(xiàn)異步加載的組件包里不小心把ECharts也打了進(jìn)去而ECharts初始化時會去解析大量主題配置和series配置這一執(zhí)行就是300ms的長任務(wù)。組件是異步了但組件內(nèi)部的初始化邏輯卻扔在了主線程上執(zhí)行——異步加載只解決了下載時機(jī)沒解決執(zhí)行開銷。這個案例給了兩個非常重要的結(jié)論第一異步加載之后一定要復(fù)查異步組件內(nèi)部的初始化邏輯。組件的JavaScript下載和執(zhí)行本身就是兩件事下載是網(wǎng)絡(luò)層面的事執(zhí)行是主線程層面的事。下載異步了但執(zhí)行時如果做了大量同步計算照樣會卡住主線程。遇到這類情況應(yīng)該把ECharts這類重組件的初始化也改為異步渲染或者Worker內(nèi)計算至少也要把初始化邏輯放在空閑時間執(zhí)行。第二異步加載的效果要通過性能數(shù)據(jù)和用戶反饋雙重驗證。我后來在優(yōu)化前后分別采集了FCP、LCP、TBT三個指標(biāo)用數(shù)據(jù)確認(rèn)了優(yōu)化方向是對的。只憑感覺變快了來評估性能優(yōu)化很容易被錯覺誤導(dǎo)。還有一個輔助手段是錄制用戶操作軌跡回放時注意觀察滾動過程有沒有卡頓掉幀這種主觀感受配合客觀數(shù)據(jù)才能準(zhǔn)確評估優(yōu)化是否成功。6.3 異步加載的邊界不要為了異步而異步聊了這么多還是要潑一盆冷水。異步加載不是越徹底越好它有一個合理邊界。判斷依據(jù)很簡單用戶在這個時間點會不會用到這個東西如果用戶登錄后第一屏就是數(shù)據(jù)看板而看板要依賴的核心數(shù)據(jù)請求被你異步到了空閑期才發(fā)那首屏就是一片空白等待——這就是異步用過頭了。同理如果某個異步組件承載的是用戶最核心的操作入口把它延后加載就需要權(quán)衡是縮短首屏加載時間重要還是保證用戶立刻能用核心功能重要另外不是所有東西都適合拆分。有些模塊雖然體積大但被多個路由共享比如公共的UI組件庫、請求庫、工具函數(shù)這樣的模塊強(qiáng)制拆分成異步chunk反而會導(dǎo)致每個頁面都要重新請求一份拷貝。這種情況下應(yīng)該把它打進(jìn)公共依賴chunk利用瀏覽器緩存長期復(fù)用。代碼分割的正確粒度應(yīng)該是模塊訪問頻率低且獨(dú)立而不是純粹按文件大小切。我做性能優(yōu)化這么長時間體會最深的一點是性能優(yōu)化的本質(zhì)是理解用戶行為把資源花在用戶最需要的地方。異步加載只是一個手段不是目的。真正合理的架構(gòu)不會為了懶加載把所有圖片全部加loadinglazy也不會為了讓首屏更快就把所有代碼全部拆成異步。它是在理解用戶的訪問路徑、理解資源的依賴關(guān)系之后做出的一個全局最優(yōu)解。7. 我沉淀的一套異步加載自檢清單按我以前帶團(tuán)隊的做法每次做完全站性能優(yōu)化Review之后會逐個頁面跑一遍自檢清單?,F(xiàn)在把它也分享出來可以作為你項目里異步加載優(yōu)化的驗收標(biāo)準(zhǔn)。[ ] 首屏LCP元素是否被誤加了懶加載LCP圖片必須顯式設(shè)置fetchpriorityhigh且立即加載。[ ] 所有懶加載圖片是否預(yù)留了寬高比能否在沒有網(wǎng)絡(luò)的情況下復(fù)現(xiàn)布局跳動[ ] 路由級代碼分割是否執(zhí)行了首屏JS體積相對優(yōu)化前下降了多少[ ] 異步組件是否有合理的加載失敗兜底是否有ErrorBoundary包裹[ ] 公共依賴是否被正確抽取而不是被打進(jìn)多個chunk[ ] 長列表場景是否使用了虛擬滾動而不是一次性渲染全部DOM[ ] 非關(guān)鍵腳本是否使用了defer/async統(tǒng)計腳本是否放到了頁面底部[ ] 有沒有做過優(yōu)化前后的Lighthouse對比FCP/LCP/TBT是否都有改善[ ] 移動端啟動階段Application.onCreate里是否還有可延后或可切換子線程的初始化任務(wù)[ ] 異步任務(wù)之間是否存在共享資源競爭線程或Worker數(shù)量是否合理這套清單我每次性能復(fù)盤都會過一遍能過濾掉大部分低級失誤。它不復(fù)雜但每一條都來自實際生產(chǎn)環(huán)境里的教訓(xùn)。異步加載這個主題看起來是單個技術(shù)點但做深了會發(fā)現(xiàn)它其實橫跨網(wǎng)絡(luò)加載、渲染機(jī)制、線程調(diào)度、框架設(shè)計多個層面是一個值得長期深耕的方向。我建議你把今天聊到的幾個方案資源異步、代碼分割、任務(wù)調(diào)度、移動端啟動優(yōu)化在自己的項目里各找一個場景試著落地然后拿數(shù)據(jù)說話再對比優(yōu)化前后的指標(biāo)變化。這個過程走完一遍你對異步加載和性能優(yōu)化的理解會比你看十篇文章都深。