在遇到一個頁面效能問題時,我理解的優化閉環是:分析、策略、驗證和沉澱。
這其中非常關鍵的一步是需要採集到效能資料,並且得有個視覺化後臺檢視資料變化。
在之前已經自制了一個效能優化平臺,採集前端效能引數的 SDK 叫 shin.js。
在文章開頭,我想先聊聊網頁優化的三部分:網路,渲染和容器。
第一部分的網路就是提升傳輸速度,可優化的手段包括 gzipped壓縮、CDN、HTTP 快取、HTTP 2.0協定、並行請求等。
像 HTTP 快取分為強快取和協商快取,請求首部和瀏覽器配合完成資源的快取機制,下圖摘自《前端程式設計師面試筆試寶典》。
第二部分的渲染就是 CRP 優化(關鍵渲染路徑),CRP 是指瀏覽器從接收資源到渲染畫素的過程。
優化的點包括資源數、位元組數和載入時序。現代化的 webpack 構建工具就會對資源做前兩項的優化處理,包括壓縮檔案、合併檔案、優化包的引入等。
載入時序就包括日常都會用的圖片懶載入和預載入、指令碼的延遲(defer)、非同步(async)和預載入(preload)等。
對於比較龐大的首頁,可以先將那些能阻塞網頁首次渲染的關鍵資源載入,其餘資源都延遲載入,以此提升頁面開啟速度。
第三部分的容器(WebView)就是借用端的能力,讓 APP 配合優化網頁。
例如預請求,將請求介面的時機前置到容器開啟之時,下面是一張實現流程圖。
還有一種靜態資源快取至使用者端本地,當時與公司使用者端討論此方案時,他們覺得每次攔截請求會損傷效能,後面就採用了折中的辦法。
就是他們去主動請求特定地址的靜態資源,然後開放介面讓我可以去讀取本地資源,也就是說由 Web 來控制是否讀取快取資源。
現在言歸正傳,回到本次的優化中來。
為了提升頁面產出率,聯合 UI 設計構建了一套可設定的通用活動模板。
活動上線後,就檢視了效能資料,情況很不理想,如下圖所示。
FP(白屏)時間大部分都在 2 秒以上,取平均值更是在 3 秒左右。Google的報告指出:
在資料庫中,將指定的效能資料記錄匯出到 Excel 中。
翻了一條後發現,效能問題集中在 DOM 中。
{ "unloadEventTime": 0, "loadEventTime": 1, "interactiveTime": 1255, "parseDomTime": 1075, "initDomTreeTime": 721, "readyStart": 5, "redirectCount": 0, "compression": 0, "redirectTime": 0, "appcacheTime": 0, "lookupDomainTime": 0, "connectSslTime": 0, "connectTime": 0, "requestTime": 119, "requestDocumentTime": 119, "responseDocumentTime": 0, "TTFB": 534, }
JSON 中的 interactiveTime、parseDomTime 和 initDomTreeTime 消耗的時間都不短,計算規則如下所示。
/** * 解析 DOM 樹結構的時間 * 期間要載入內嵌資源 * 反省下你的 DOM 樹巢狀是不是太多了 */ api.parseDomTime = timing.domComplete - timing.domInteractive; /** * 請求完畢至DOM載入耗時 */ api.initDomTreeTime = timing.domInteractive - timing.responseEnd; /** * 首次可互動時間 */ api.interactiveTime = timing.domInteractive - timing.fetchStart;
參考 W3C 第二版效能引數圖可知,慢的地方集中在 Processing 階段。
開啟 Chrome DevTools 中的 Performance 一欄,錄製後,可在火焰圖中看到長任務。
點選 Long task 連結,會跳轉到使用 RAIL 模型衡量效能一文。
在 PC 瀏覽器中開啟肯定會比在手機中快,但即使如此,還是出現了效能瓶頸,說明這裡是真的慢。
藍底的 DCL 是 DOMContentLoaded 事件,在 HTML 檔案被完全載入和解析後觸發,綠底的 FP 就是白屏時間。
黃底的 Evaluate Script 表示載入 JavaScript 指令碼,Compile Script 表示執行 JavaScript 指令碼。
再來看看網路請求瀑布圖,下圖中的藍線就是 DCL,可以清晰的看到,藍線之前在載入的基本都是 JavaScript 指令碼。
由此可知,載入的指令碼有點多,並且有一個 chunk-vendors.js 指令碼還比較大,下載時間有點長(依據藍色塊)。
定位到了問題根源,那就直接檢視基於 Vue 的程式碼是怎麼寫的了。
1)HTML
下面是編譯後的頁面 HTML 結構,只列出了關鍵部分。
<!DOCTYPE html> <html lang=en> <head> <script src=https://res.wx.qq.com/open/js/jweixin-1.6.0.js></script> <script src=//www.xxxx.com/flexible/flexible.js></script> <script src=//www.xxxx.co/files/js/baidu.js></script> <script src=//www.xxxx.co/files/js/shin.js></script> <link href=//www.xxxx.me/game/css/operation37.cba04f10.css rel=preload as=style> <link href=//www.xxxx.me/game/js/chunk-lodash.152ef24b.js rel=preload as=script> <link href=//www.xxxx.me/game/js/chunk-lottie.23b9982e.js rel=preload as=script> <link href=//www.xxxx.me/game/js/operation37.fa5f5378.js rel=preload as=script> <link href=//www.xxxx.me/game/css/chunk-vendors.779f7d1d.css rel=stylesheet> <link href=//www.xxxx.me/game/css/operation37.cba04f10.css rel=stylesheet> </head> <body> <div id=app></div> <script src=//www.xxxx.me/game/js/chunk-vendors.ca022e99.js></script> <script src=//www.xxxx.me/game/js/operation37.fa5f5378.js></script> </body> </html>
首先在 head 中,引入了大量的 JavaScript 指令碼,flexible.js 其實在構建時可以內聯,不需要網路存取。
然後 jweixin-1.6.0.js 和 baidu.js 這兩個指令碼完全可以延遲載入,後者就是增加百度統計的指令碼。
接著就是 shin.js 需要做壓縮處理,可以減少 50% 以上的尺寸。
在 link 元素中,使用了 preload,表示可並行的預載入,並且不會執行,這是提升頁面效能的一種手段。
雖然第三方的庫(chunk-vendors.ca022e99.js)和業務主邏輯(operation37.fa5f5378.js)兩個指令碼宣告在 body 中。
但是主結構就是個空的 div,因此在載入和執行時就會延長 DOM 的解析,影響白屏時間。
2)vendors 優化
Vue 內建了一條命令,可以檢視每個指令碼的尺寸以及內部依賴包的尺寸。
在下圖中,vendors.js 的原始尺寸是 3.76M,gzipped 壓縮後的尺寸是 442.02KB,比較大的包是 lottie、swiper、moment、lodash 等。
這類比較大的包可以再單獨剝離,不用全部聚合在 vendors.js 中。
在 vue.config.js 中,設定 config.optimization.splitChunks(),如下所示,引數含義可參考官網。
config.optimization.splitChunks( { cacheGroups: { vendors: { name: 'chunk-vendors', test: /[\\/]node_modules[\\/]/, priority: -10, chunks: 'initial' }, lottie: { name: 'chunk-lottie', test: /[\\/]node_modules[\\/]lottie-web[\\/]/, chunks: 'all', priority: 3, reuseExistingChunk: true, enforce: true }, swiper: { name: 'chunk-swiper', test: /[\\/]node_modules[\\/][email protected]@swiper[\\/]/, chunks: 'all', priority: 3, reuseExistingChunk: true, enforce: true }, lodash: { name: 'chunk-lodash', test: /[\\/]node_modules[\\/]lodash[\\/]/, chunks: 'all', priority: 3, reuseExistingChunk: true, enforce: true } } } )
在經過一頓初步操作後,原始尺寸降到 2.4M,gzipped 壓縮後的尺寸是 308.64KB,比之前少了 100 多 KB。
現在在入口處需要單獨宣告依賴的包,否則不會自動引入。
pages: { operation37: { entry: 'src/pages/operation37/index.js', template: 'src/pages/operation37/index.html', filename: 'operation37.html', title: '榜單設定頁面', chunks: ['chunk-lottie', 'operation37', 'chunk-vendors'] }, }
其實大部分的 H5 頁面都比較簡單,可能就使用了包的一個小功能,那完全可以自己用程式碼實現,這樣就不必依賴那個大包了。
後面就是在程式碼邏輯層面的優化,核心就是減少指令碼尺寸。優化後,再去觀察資料的變化。
3)CDN加速
之前部分靜態資源採取了 CDN 加速,現在需要將 game 下面中的靜態資源全部走 CDN。
在雲端設定些引數,就能走 CDN。不過,第一次沒有設定好,沒有設定轉發路徑,造成了嚴重的線上問題。
第二次就比較謹慎,在測試環境將之前碰到的問題都驗證後,才最終線上上設定。
白屏時間佔比變化:
參考資料: