Lighthouse 簡介
Lighthouse 是一個開源的自動化工具,可以用於改進 Web 應用的質量。
Lighthouse 目前已經整合在新版本 Chrome DevTools 中,也可以將其作為一個 Chrome 擴充套件程式執行,或從命令列執行。
Lighthouse 會針對 Web 頁面執行一些自動化測試,生成一個有關頁面效能的報告,然後就可以根據測試報告來優化頁面。
Lighthouse 測試報告包含五塊內容:效能、無障礙、最佳做法、SEO、PWA,我們今天主要是針對效能報告提出優化方案及復現程式碼。
Chrome DevTools 可以調整語言喲,比如說從英文改成中文。
Lighthouse 效能指標
- First Contentful Paint
FCP 指首次內容渲染時間,標識網頁首次渲染出首個文字、圖片(頁面上的影像、非白色<canvas>
元素和SVG
被視為 DOM 內容。不包括 iframe 內的任何內容)的時間。 FCP 分點陣圖。 - Time to Interactive
TTI 指可互動時間,標識網頁需要多長時間才能提供完整互動功能。我們知道 JS 主執行緒是單執行緒的,如果有長 JS 任務是會阻塞頁面互動 - Speed Index
速度指數表明了網頁內容的可見填充速度 - Total Blocking Time
TBT 指累計阻塞時間,標識網頁首次內容渲染 (FCP) 和可互動時間之間的所有超時任務的超時累計時間,按 60FPS 算,時間應該是不超過 16ms
超時任務指任務用時超過 50ms,如果 Lighthouse 檢測到一個 70ms 長的任務,則阻塞部分將為 20ms。 - Largest Contentful Paint
LCP 指 最大內容繪製,標識網頁渲染出最大文字或圖片的時間。類似於 First Meaningful Paint(FMP 首次有效繪製)(主要內容對使用者可見的時間) 指標,但是 LCP 是一個通用固定計算規則。 Cumulative Layout Shift
CLS 指累積佈局偏移,標識網頁可見元素在視口內的移動情況。計算規則- 比如說在網上閱讀一篇文章,結果頁面上的某些內容突然發生改變?文字在毫無預警的情況下移位,導致您找不到先前閱讀的位置。
- 比如正要點選一個連結或一個按鈕,但在手指落下的瞬間,誒?連結移位了,結果點到了別的東西!大多數情況下,這些體驗只是令人惱火,但在某些情況下,卻可能帶來真正的破壞(點拒絕結果變成了允許)。
- 或者想想變態貓?看著是平平無奇,但是操作的時候飛來橫禍。
LCP、FCP 示例
Lighthouse 優化建議
建議一:減少未使用的 JavaScript、CSS 程式碼
手段1:非同步。等需要時再載入
名詞有很多:懶載入、延時載入、閒時載入、按需載入等等。
「懶載入」import 非同步元件
- 比如說我們有一個訊息中心模組,內部有使用者端和管理端頁面。這兩個頁面應該使用非同步元件。因為目的不是發就是收,而且管理端是需要許可權才可以看到的。
- 比如說我們的訊息模組,支援 markdown、富文字、docs、xlsx 等等型別,但是一條訊息只能顯示一個型別,所以我們可以把 DocsView 來封裝一下,動態載入元件渲染。
「懶載入」import 非同步功能
- 比如說我們的表格有匯出功能,基於 xlsx 實現。這樣一個功能也不是高頻功能,所以我們也可以通過 import 來非同步載入使用。
「延時載入」通過手動調整優先順序、或者延時器來實現功能。
需要注意 CLS 指標,這裡需要注意不要造成 CLS 指標異常。- 還是我們訊息模組的例子,在 LCP 之前我們不去載入是否有最新訊息,等 LCP 之後再去載入。這裡對於首屏減少了請求,對於使用者的影響也會比較小 (只顯示一個紅點,使用者有紅點和無紅點的點選效果都是檢視訊息列表)。
- 對於關注、點贊、收藏等開關模組需要慎重。因為效果是相反的,本來是關注,結果有可能變成了取消。
手段2:搖樹優化 Tree-Shaking
可以將未使用的程式碼邏輯在編譯時刪除。
因為 Webpack 4 是預設開啟狀態,所以我們只說一些限制條件。
- 只對 ES Module 起作用,對於 commonjs 無效,對於 umd 亦無效。
- 需要包本身支援才可以。
- 注意配置 sideEffects ,防止 Css 被優化
- 需要 build 時才會優化。process.env.NODE_ENV === 'production' 狀態
手段3:babel 降低適配版本
設定合理的 .browserslistrc,進行 babel、babel-polyfill 轉義。
比如說只支援 Chrome 的後臺系統,就不需要轉義 IE 系列了,這樣可以大大減小體積。
總結&注意事項
- css 的優化,也可以依賴非同步元件。
- 三方庫可以考慮使用元件做二次封裝。
- 正確的區分 v-if 和 v-show,深入研究 el-tabs 實現機制。確保元件並沒有被真實的例項化
- 可以通過 chrome-devtools 中 coverage 來檢視哪些程式碼沒有被執行。
可以分析具體有哪些程式碼沒有被執行到,我們期望的結果是載入的每一行程式碼都是有用的。
可以看到有一個資源一大半程式碼都沒有被用到,這都是浪費。如果頻寬是按量付費的人得哭死。
last 2 verions
表示支援所有版本後兩位。也包括永遠不更新的 IE
建議二:優化體積、消除重複程式碼
手段1:壓縮
- 資源伺服器開始 Gzip 壓縮,設定資源 30D 快取。然後通過檔名 hash 來更新
- CDN 一般來說都會支援壓縮和快取,並且頻寬也是比較靠譜的,節點距離使用者也比較近。
- jsmin、搖樹優化等方案。
- 注意圖片型別。純色圖片 png,複雜圖片 jpg,webp 更小。
- 注意圖片尺寸。儘量不要過大,注意裁圖。
手段2:消除重複包。依賴共享
lerna + yarn
常見重複包(axios、ui 庫),因為我們好多元件都是基於業務封裝(什麼叫基於業務封裝?內部有邏輯,存在資料呼叫自動更新,UI 可以直接在業務內使用)。
我們使用了 lerna 來做相同版本共享,我們一般要求使用相同版本。peerDependencies
因為某些原因,有一些包並不是所有專案都有的,或者說對於版本有強依賴。我們也需要配置peerDependencies
來讓使用方可以安裝正確版本的依賴。externals
部分三方庫版本號是0.xx.x
,因為lerna
是基於首位不為 0 的進行比較是否為相同版本。導致axios@0.23
和axios@0.24
不認為是相同版本。
這個時候我們會使用externals
來強制不打包,讓使用方來提供。- 統一構建工具及版本
因為有時候我們會有一些基礎包(babel
、babel-polyfill
),但是各個cli
版本使用的版本、方式不一樣,對於轉義處理也是不一致的,為了使用最小的包體積,我們要求相同相同版本的cli
webpack
。
手段3:替換包,選擇更小的版本
小包替換大包,按需包替換全量包
momentjs
改為使用dayjs
,momentjs
包只使用format
大概在100k
左右,而dayjs
只有10k
不到。
這是因為momentjs
裡面打入了i18n
語言包。
所以還有另一個方案就是改成語言按需引入。- 避免 import _ from 'lodash ,而是使用 lodash-es。
建議三:減少重排、減少佈局變動、減少 DOM 數量
可以理解為 CLS 指標。
手段1:使用固定寬高
一般來說都有固定高度(24*24
),我們把它限制在一個範圍內。防止因為圖片非同步載入回來,撐開 dom 後,導致其後資料全部發生變動。
- 比如說在文章類頁面中,如果有記住上次瀏覽位置。如何保證使用者還能定位到上次位置?如果不使用固定寬高,那麼使用者有可能看到的內容會一直發現變化。
- 比如說在微信聊天頁面,如何一直定位在頁面最底部?當圖片載入完成之後會出現高度變化。
手段2:虛擬化、虛擬列表、墓碑機制、分頁載入、懶載入
虛擬列表類似實現一個最差邊界方案,我只顯示 20 個,這就是我效能最差的時候。不管 100 個、1000 個,我只顯示 20 個。
select、table、tree 等長列表注意使用虛擬列表。
- 比如說微信聊天,左邊的會話列表大家會刪除嘛?估計會有幾百、幾千個,包括單聊、群聊、通知、公眾號等等。
會有幾個節點?頭像、名稱、訊息摘要、時間、遮蔽、未讀等等,就算會有 10 個標籤。10 * 1000
這樣就一萬個標籤了,如果使用虛擬列表,10 * 50
這樣也才 500 個標籤。 - 比如說有一些樹節點,層級覆蓋下去有可能會在幾十萬個節點,如果再操作選中之類的邏輯
- 比如說微信聊天,左邊的會話列表大家會刪除嘛?估計會有幾百、幾千個,包括單聊、群聊、通知、公眾號等等。
異常的 tooltip 節點。
- 比如說會提前渲染元件。
- 比如說會進行頻繁變更。
建議四:降低記憶體佔用、降低 CPU 使用率
手段1:圖片懶載入
圖片會佔用實際記憶體,導致卡頓。
- 注意圖片尺寸。儘量不要過大,注意裁圖。
- 只載入視口的圖片。其他圖片按需載入,參考文章頁面返回上次閱讀邏輯功能,如果不按需會導致無法檢視圖片。
手段2:減少 JS 程式碼、減少 CSS 程式碼
參考上面的邏輯就好了。
程式碼減少,下載、解析、執行的時候當然都會減少呀。
建議您減少為解析、編譯和執行 JS 而花費的時間。您可能會發現,提供較小的 JS 負載有助於實現此目標。
建議五:降低網路負載、加快使用者下載速度
資源下載速度限制條件一般有什麼?
- 頻寬(吞吐)1M小水管
- 距離(遠近)華北地方內訪問、全球訪問
- 介質(穩定)有線、無線、WiFi、5G、4G
手段1:增加頻寬
需要充錢才能解決的問題都不予解決?
如果是伺服器頻寬不夠,那麼解決辦法就是充錢。
如果是使用者頻寬不夠,那麼只能好好優化。
手段2:上 CDN
CDN 的關鍵技術主要有內容儲存和分發技術,使使用者就近獲取所需內容,降低網路擁塞,提高使用者訪問響應速度和命中率。
簡單來說就是距離使用者近,頻寬大,網路暢通不擁塞。
手段3:縮小體積
(同建議一、建議二)
手段4:增加快取
前端專案一般 html 不快取,然後資源通過 hash name 來更新。
世界級別的難題:如何讓快取過期和如何讓快取不過期。
建議六:縮短執行時間、縮短執行鏈路、避免阻塞主執行緒
手段1:減少主執行緒工作,優化程式碼執行速度
- 正確使用迴圈。應該先執行
filter
,再執行map
。
因為 filter 之後,數量會變少(隨機數,變成一半),兩個示例等於 1.5倍 和 2倍 的對比。
那麼有什麼辦法可以 1 倍出結果嗎?(reduce?)
- 正確使用迴圈。如果不使用結果,應該使用 forEach 遍歷。
- 優化巢狀迴圈。上面的例子充其量就是 N * 2,巢狀迴圈就會變成 N²。一般出現在樹狀結構,比如說許可權。
例子採用去重來說明差距
使用 lodash 中的方法來簡化資料處理邏輯。
可以實現程式碼少、效率高、語義明確。Object.entries(treeData).filter(([, v]) => v.level === 0).forEach(([key]) => {
這行程式碼想做什麼?有什麼效能上的問題嗎?
Object.entries(treeData)
的效能偏差,tree
節點量級是多少?考慮用 lodash 提供的方法,效能會好一點(迴圈次數變少,3次降成1次)、程式碼變得更少了(減少了無用的語義)、語義也更加明確了(把物件轉換成keyvalue陣列,過濾掉有level的,遍歷執行邏輯 => 遍歷物件)。
手段2:減少序列程式碼,縮短請求鏈路
- await、async 的亂用
建議七:避免過時的程式碼
Vue、react 之類框架程式碼還好,有統一的管理方式。常見的是一些老專案、js、jQuery 專案。
- 禁止
document.write
- 樣式放上面、JS 放下面。不要阻塞 DOM 解析。
總結
優化之路,相輔相成。
- 優化載入速度。擴大頻寬、快取、內容分發、減小體積。
- 優化體積。減少損耗,減少解析時間。
- 優化邏輯。懶載入、非同步載入、減少無效損耗、提升執行效率。
專案實戰
我提供了一些最簡案例在倉庫中:Demo 倉庫地址,你只需要看例子就可以直接看到問題(然後你可以去優化它)。