寫在 2021 的前端效能優化指南
我們把效能優化的方向分為以下兩個方面,有助於結構化的思考與系統分析。
- 載入效能。如何更快地把資源從伺服器中拉到瀏覽器,如 http 與資源體積的各種優化,都是旨在載入效能的提升。
- 渲染效能。如何更快的把資源在瀏覽器上進行渲染。如減少重排重繪,rIC 等都是旨在渲染效能的提升。
核心效能指標與 Performance API
- LCP: 載入效能。最大內容繪製應在 2.5s 內完成。
- FID: 互動效能。首次輸入延遲應在 100ms 內完成。
- CLS: 頁面穩定性。累積佈局偏移,需手動計算,CLS 應保持在 0.1 以下。
計算與收集
當收集瀏覽器端每個使用者核心效能指標時,可通過web-vitals
import { getCLS, getFID, getLCP } from 'web-vitals'
function sendToAnalytics(metric) {
const body = JSON.stringify(metric);
navigator.sendBeacon('/analytics', body))
}
getCLS(sendToAnalytics);
getFID(sendToAnalytics);
getLCP(sendToAnalytics);
複製程式碼
更快的傳輸: CDN
將資源分發到 CDN 的邊緣網路節點,使使用者可就近獲取所需內容,大幅減小了光纖傳輸距離,使全球各地使用者開啟網站都擁有良好的網路體驗。
更快的傳輸: http2
http2
的諸多特性決定了它更快的傳輸速度。
- 多路複用,在瀏覽器可並行傳送 N 條請求。
- 首部壓縮,更小的負載體積。
- 請求優先順序,更快的關鍵請求
目前,網站已大多上了 http2,可在控制檯面板進行檢視。
由於 http2 可並行請求,解決了 http1.1 線頭阻塞的問題,以下幾個效能優化點將會過時
- 資源合併。如
https://shanyue.tech/assets??index.js,interview.js,report.js
- 域名分片。
- 雪碧圖。將無數小圖片合併成單個大圖片。
更快的傳輸: 充分利用 HTTP 快取
更好的資源快取策略,對於 CDN 來講可減少回源次數,對於瀏覽器而言可減少請求傳送次數。無論哪一點,對於二次網站訪問都具有更好的訪問體驗。
- 快取策略
- 強快取: 打包後帶有 hash 值的資源 (如 /build/a3b4c8a8.js)
- 協商快取: 打包後不帶有 hash 值的資源 (如 /index.html)
- 分包載入 (Bundle Spliting)
- 避免一行程式碼修改導致整個 bundle 的快取失效
更快的傳輸: 減少 HTTP 請求及負載
對一個網站的資源進行壓縮優化,從而達到減少 HTTP 負載的目的。
- js/css/image 等常規資源體積優化,這是一個大話題,再以下分別討論
- 小圖片優化,將小圖片內聯為 Data URI,減小請求數量
- 圖片懶載入
- 新的 API: IntersectionObserver API
- 新的屬性: loading=lazy
更小的體積: gzip/brotli
對 JS、CSS、HTML 等文字資源均有效,但是對圖片效果不大。
gzip
通過 LZ77 演算法與 Huffman 編碼來壓縮檔案,重複度越高的檔案可壓縮的空間就越大。brotli
通過變種的 LZ77 演算法、Huffman 編碼及二階文字建模來壓縮檔案,更先進的壓縮演算法,比 gzip 有更高的效能及壓縮率
可在瀏覽器的Content-Encoding
響應頭檢視該網站是否開啟了壓縮演算法,目前知乎、掘金等已全面開啟了brotli
壓縮。
# Request Header
Accept-Encoding: gzip, deflate, br
# gzip
Content-Encoding: gzip
# gzip
Content-Encoding: br
複製程式碼
更小的體積: 壓縮混淆工具
Terser是 Javascript 資源壓縮混淆的神器。
它可以根據以下策略進行壓縮處理:
- 長變數名替換短變數
- 刪除空格換行符
- 預計算:
const a = 24 * 60 * 60 * 1000
->const a = 86400000
- 移除無法被執行的程式碼
- 移除無用的變數及函式
可在Terser Repl線上檢視程式碼壓縮效果。
- swc是另外一個用以壓縮 Javascript 的工具,它擁有與
terser
相同的 API,由於它是由rust
所寫,因此它擁有更高的效能。 - html-minifier-terser用以壓縮 HTML 的工具
更小的體積: 更小的 Javascript
關於更小的 Javascript,上邊已總結了兩條:
- gzip/brotli
- terser (minify)
還有以下幾點可以考慮考慮:
- 路由懶載入,無需載入整個應用的資源
- Tree Shaking: 無用匯出將在生產環境進行刪除
- browserlist/babel: 及時更新 browserlist,將會產生更小的墊片體積
再補充一個問題:
如何分析並優化當前專案的 Javascript 體積?如果使用webpack
那就簡單很多。
- 使用
webpack-bundle-analyze
分析打包體積 - 對一些庫替換為更小體積的庫,如 moment -> dayjs
- 對一些庫進行按需載入,如
import lodash
->import lodash/get
- 對一些庫使用支援 Tree Shaking,如
import lodash
->import lodash-es
更小的體積: 更小的圖片
在前端發展的現在,webp
普遍比jpeg/png
更小,而avif
又比webp
小一個級別
為了無縫相容,可選擇picture/source
進行回退處理
<picture>
<source srcset="img/photo.avif" type="image/avif">
<source srcset="img/photo.webp" type="image/webp">
<img src="img/photo.jpg" width="360" height="240">
</picture>
複製程式碼
- 更合適的尺寸: 當頁面僅需顯示 100px/100px 大小圖片時,對圖片進行壓縮到 100px/100px
- 更合適的壓縮: 可對前端圖片進行適當壓縮,如通過
sharp
等
渲染優化: 關鍵渲染路徑
以下五個步驟為關鍵渲染路徑
- HTML -> DOM,將 html 解析為 DOM
- CSS -> CSSOM,將 CSS 解析為 CSSOM
- DOM/CSSOM -> Render Tree,將 DOM 與 CSSOM 合併成渲染樹
- RenderTree -> Layout,確定渲染樹中每個節點的位置資訊
- Layout -> Paint,將每個節點渲染在瀏覽器中
渲染的優化很大程度上是對關鍵渲染路徑進行優化。
preload/prefetch
preload
/prefetch
可控制 HTTP 優先順序,從而達到關鍵請求更快響應的目的。
<link rel="prefetch" href="style.css" as="style">
<link rel="preload" href="main.js" as="script">
複製程式碼
- preload 載入當前路由必需資源,優先順序高。一般對於 Bundle Spliting 資源與 Code Spliting 資源做 preload
- prefetch 優先順序低,在瀏覽器 idle 狀態時載入資源。一般用以載入其它路由資源,如當頁面出現 Link,可 prefetch 當前 Link 的路由資源。(next.js 預設會對 link 做懶載入+prefetch,即當某條 Link 出現頁面中,即自動 prefetch 該 Link 指向的路由資源
捎帶說一下dns-prefetch
,可對主機地址的 DNS 進行預解析。
<link rel="dns-prefetch" href="//shanyue.tech">
複製程式碼
渲染優化: 防抖與節流
- 防抖:防止抖動,單位時間內事件觸發會被重置,避免事件被誤傷觸發多次。程式碼實現重在清零 clearTimeout。防抖可以比作等電梯,只要有一個人進來,就需要再等一會兒。業務場景有避免登入按鈕多次點選的重複提交。
- 節流:控制流量,單位時間內事件只能觸發一次,與伺服器端的限流 (Rate Limit) 類似。程式碼實現重在開鎖關鎖 timer=timeout; timer=null。節流可以比作過紅綠燈,每等一個紅燈時間就可以過一批。
無論是防抖還是節流都可以大幅度減少渲染次數,在 React 中還可以使用use-debounce
之類的 hooks 避免重新渲染。
import React, { useState } from 'react';
import { useDebounce } from 'use-debounce';
export default function Input() {
const [text, setText] = useState('Hello');
// 一秒鐘渲染一次,大大降低了重新渲染的頻率
const [value] = useDebounce(text, 1000);
return (
<div>
<input
defaultValue={'Hello'}
onChange={(e) => {
setText(e.target.value);
}}
/>
<p>Actual value: {text}</p>
<p>Debounce value: {value}</p>
</div>
);
}
複製程式碼
渲染優化: 虛擬列表優化
這又是一個老生常談的話題,一般在視口內維護一個虛擬列表(僅渲染十幾條條資料左右),監聽視口位置變化,從而對視口內的虛擬列表進行控制。
在 React 中可採用以下庫:
渲染優化: 請求及資源快取
在一些前端系統中,當載入頁面時會發送請求,路由切換出去再切換回來時又會重新發送請求,每次請求完成後會對頁面重新渲染。
然而這些重新請求再大多數時是沒有必要的,合理地對 API 進行快取將達到優化渲染的目的。
- 對每一條 GET API 新增 key
- 根據 key 控制該 API 快取,重複發生請求時將從快取中取得
function Example() {
// 設定快取的 Key 為 Users:10086
const { isLoading, data } = useQuery(['users', userId], () => fetchUserById(userId))
}
複製程式碼
Web Worker
試舉一例:
在純瀏覽器中,如何實現高效能的實時程式碼編譯及轉換?
如果純碎使用傳統的 Javascript 實現,將會耗時過多阻塞主執行緒,有可能導致頁面卡頓。
如果使用Web Worker
交由額外的執行緒來做這件事,將會高效很多,基本上所有在瀏覽器端進行程式碼編譯的功能都由Web Worker
實現。
WASM
- JS 效能低下
- C++/Rust 高效能
- 使用 C++/Rust 編寫程式碼,然後在 Javascript 環境執行
試舉一例:
在純瀏覽器中,如何實現高效能的圖片壓縮?
基本上很難做到,Javascript 的效能與生態決定了實現圖片壓縮的艱難。
而藉助於 WASM 就相當於借用了其它語言的生態。
- libavif: C語言寫的 avif 解碼編碼庫
- libwebp: C語言寫的 webp 解碼編碼庫
- mozjpeg: C語言寫的 jpeg 解碼編碼庫
- oxipng: Rust語言寫的 png 優化庫
而由於 WASM,完全可以把這些其它語言的生態移植到瀏覽器中,從而實現一個高效能的離線式的圖片壓縮工具。
如果想了解這種的工具,請看看squoosh
文章分類