1. 程式人生 > 其它 >寫在 2021 的前端效能優化指南

寫在 2021 的前端效能優化指南

我們把效能優化的方向分為以下兩個方面,有助於結構化的思考與系統分析。

  1. 載入效能。如何更快地把資源從伺服器中拉到瀏覽器,如 http 與資源體積的各種優化,都是旨在載入效能的提升。
  2. 渲染效能。如何更快的把資源在瀏覽器上進行渲染。如減少重排重繪,rIC 等都是旨在渲染效能的提升。

核心效能指標與 Performance API

  • LCP: 載入效能。最大內容繪製應在 2.5s 內完成。
  • FID: 互動效能。首次輸入延遲應在 100ms 內完成。
  • CLS: 頁面穩定性。累積佈局偏移,需手動計算,CLS 應保持在 0.1 以下。

計算與收集

當收集瀏覽器端每個使用者核心效能指標時,可通過web-vitals

收集並通過sendBeacon上報到打點系統。

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的諸多特性決定了它更快的傳輸速度。

  1. 多路複用,在瀏覽器可並行傳送 N 條請求。
  2. 首部壓縮,更小的負載體積。
  3. 請求優先順序,更快的關鍵請求

目前,網站已大多上了 http2,可在控制檯面板進行檢視。

由於 http2 可並行請求,解決了 http1.1 線頭阻塞的問題,以下幾個效能優化點將會過時

  1. 資源合併。如https://shanyue.tech/assets??index.js,interview.js,report.js
  2. 域名分片。
  3. 雪碧圖。將無數小圖片合併成單個大圖片。

更快的傳輸: 充分利用 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 資源壓縮混淆的神器。

它可以根據以下策略進行壓縮處理:

  1. 長變數名替換短變數
  2. 刪除空格換行符
  3. 預計算:const a = 24 * 60 * 60 * 1000->const a = 86400000
  4. 移除無法被執行的程式碼
  5. 移除無用的變數及函式

可在Terser Repl線上檢視程式碼壓縮效果。

  1. swc是另外一個用以壓縮 Javascript 的工具,它擁有與terser相同的 API,由於它是由rust所寫,因此它擁有更高的效能。
  2. html-minifier-terser用以壓縮 HTML 的工具

更小的體積: 更小的 Javascript

關於更小的 Javascript,上邊已總結了兩條:

  1. gzip/brotli
  2. terser (minify)

還有以下幾點可以考慮考慮:

  1. 路由懶載入,無需載入整個應用的資源
  2. Tree Shaking: 無用匯出將在生產環境進行刪除
  3. browserlist/babel: 及時更新 browserlist,將會產生更小的墊片體積

再補充一個問題:

如何分析並優化當前專案的 Javascript 體積?如果使用webpack那就簡單很多。

  1. 使用webpack-bundle-analyze分析打包體積
  2. 對一些庫替換為更小體積的庫,如 moment -> dayjs
  3. 對一些庫進行按需載入,如import lodash->import lodash/get
  4. 對一些庫使用支援 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>
複製程式碼
  1. 更合適的尺寸: 當頁面僅需顯示 100px/100px 大小圖片時,對圖片進行壓縮到 100px/100px
  2. 更合適的壓縮: 可對前端圖片進行適當壓縮,如通過sharp

渲染優化: 關鍵渲染路徑

以下五個步驟為關鍵渲染路徑

  1. HTML -> DOM,將 html 解析為 DOM
  2. CSS -> CSSOM,將 CSS 解析為 CSSOM
  3. DOM/CSSOM -> Render Tree,將 DOM 與 CSSOM 合併成渲染樹
  4. RenderTree -> Layout,確定渲染樹中每個節點的位置資訊
  5. Layout -> Paint,將每個節點渲染在瀏覽器中

渲染的優化很大程度上是對關鍵渲染路徑進行優化。

preload/prefetch

preload/prefetch可控制 HTTP 優先順序,從而達到關鍵請求更快響應的目的。

<link rel="prefetch" href="style.css" as="style">
<link rel="preload" href="main.js" as="script">
複製程式碼
  1. preload 載入當前路由必需資源,優先順序高。一般對於 Bundle Spliting 資源與 Code Spliting 資源做 preload
  2. prefetch 優先順序低,在瀏覽器 idle 狀態時載入資源。一般用以載入其它路由資源,如當頁面出現 Link,可 prefetch 當前 Link 的路由資源。(next.js 預設會對 link 做懶載入+prefetch,即當某條 Link 出現頁面中,即自動 prefetch 該 Link 指向的路由資源

捎帶說一下dns-prefetch,可對主機地址的 DNS 進行預解析。

<link rel="dns-prefetch" href="//shanyue.tech">
複製程式碼

渲染優化: 防抖與節流

  1. 防抖:防止抖動,單位時間內事件觸發會被重置,避免事件被誤傷觸發多次。程式碼實現重在清零 clearTimeout。防抖可以比作等電梯,只要有一個人進來,就需要再等一會兒。業務場景有避免登入按鈕多次點選的重複提交。
  2. 節流:控制流量,單位時間內事件只能觸發一次,與伺服器端的限流 (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 中可採用以下庫:

  1. react-virtualized
  2. react-window

渲染優化: 請求及資源快取

在一些前端系統中,當載入頁面時會發送請求,路由切換出去再切換回來時又會重新發送請求,每次請求完成後會對頁面重新渲染。

然而這些重新請求再大多數時是沒有必要的,合理地對 API 進行快取將達到優化渲染的目的。

  1. 對每一條 GET API 新增 key
  2. 根據 key 控制該 API 快取,重複發生請求時將從快取中取得
function Example() {
  // 設定快取的 Key 為 Users:10086
  const { isLoading, data } = useQuery(['users', userId], () => fetchUserById(userId))
}
複製程式碼

Web Worker

試舉一例:

在純瀏覽器中,如何實現高效能的實時程式碼編譯及轉換?

  1. Babel Repl

如果純碎使用傳統的 Javascript 實現,將會耗時過多阻塞主執行緒,有可能導致頁面卡頓。

如果使用Web Worker交由額外的執行緒來做這件事,將會高效很多,基本上所有在瀏覽器端進行程式碼編譯的功能都由Web Worker實現。

WASM

  1. JS 效能低下
  2. C++/Rust 高效能
  3. 使用 C++/Rust 編寫程式碼,然後在 Javascript 環境執行

試舉一例:

在純瀏覽器中,如何實現高效能的圖片壓縮?

基本上很難做到,Javascript 的效能與生態決定了實現圖片壓縮的艱難。

而藉助於 WASM 就相當於借用了其它語言的生態。

  1. libavif: C語言寫的 avif 解碼編碼庫
  2. libwebp: C語言寫的 webp 解碼編碼庫
  3. mozjpeg: C語言寫的 jpeg 解碼編碼庫
  4. oxipng: Rust語言寫的 png 優化庫

而由於 WASM,完全可以把這些其它語言的生態移植到瀏覽器中,從而實現一個高效能的離線式的圖片壓縮工具。

如果想了解這種的工具,請看看squoosh

文章分類