為什麼要升級 HTTP#

許多前端效能優化技巧都帶有「不一定見效、有時甚至會反效果」的性質,但 HTTP 這個應用層協定的升級卻是少數例外:

重點原則 只要環境允許,就盡量把 HTTP 升上去,因為帶來的效能提升是非常顯著的。

雖然版本升級主要靠後端與基礎建設配合,但前端開發者每天都在跟 HTTP 打交道,HTTP/1.1、HTTP/2、HTTP/3 之間的差異也直接影響到許多既有的前端優化策略,因此理解這些網路觀念對前端工程師同樣重要。

HTTP/1.1 的限制#

以下假設讀者已熟悉 TCP(Transmission Control Protocol)建立連線的三次握手與斷線時的四次揮手。HTTP/1.0 因為已經太過古老,不再贅述,這裡直接從仍廣泛使用的 HTTP/1.1 看起。

限制一:Head-of-Line Blocking#

HTTP/1.1 只能以串行(serial)方式處理請求,後面的請求必須等前一個請求完成才能發出去。

為了改善這個情況,曾出現一個叫做 HTTP pipelining 的機制,允許一次送出一組請求,但是回應仍必須按發送順序回傳。例如同時發出兩個請求,第二個本來只需要 300 毫秒,但若第一個花了 5 秒,第二個就只能在後面排隊等候,這就是所謂的 Head-of-Line Blocking(HOL Blocking)。

加上 HTTP pipelining 還有「只支援 GET 與 HEAD」、「proxy 支援度差」等限制,最後幾乎宣告失敗,主流瀏覽器也預設關閉這個機制。

限制二:瀏覽器的同網域連線數限制#

既然串行那麼慢,乾脆多開幾個連線就好?實際上不行——瀏覽器會限制同一個網域同時可建立的 TCP 連線數。例如 Google Chrome 對單一網域的同時請求上限是 6 條、總連線上限為 10 條,超過時請求會被丟到佇列裡排隊。

觀察方式 在 DevTool 的 Network waterfall 欄位可以看到每個請求停留在 queueing 階段的時間,這正是被連線數上限卡住的證據。

限制三:Header 無法壓縮#

在 HTTP/1.x 中,請求與回應的 header 沒辦法壓縮。一個 header 可能就有 2 至 3 KB,且各個請求之間還有大量重複欄位,沒有壓縮機制下會持續耗損頻寬與時間。

由限制衍生的前端優化技巧#

由於上述限制,過去衍生出一條原則:「盡量減少 HTTP 請求數量。」具體做法包括:

  • CSS Sprites:把多張小圖合併成一張大圖。
  • Bundling:把多個 CSS / JS 模組合併打包。
  • Domain Sharding:刻意把資源拆分到不同網域,繞開瀏覽器的同網域連線數限制。

這些技巧在 HTTP/1.x 時代相當有用,但也帶來檔案維護、快取粒度等代價。

HTTP/2#

HTTP/2(簡稱 H2)目前已被廣泛使用,與 HTTP/1.x 相比最大的提升就是「效能」。它主要解決了兩個問題:

  • HTTP pipelining 的 Head-of-Line Blocking
  • Header 無法壓縮

容易誤解的地方 HTTP/2 並沒有解決 TCP 層級的 Head-of-Line Blocking,它解決的只是 HTTP pipelining 那一層的阻塞。

Header 壓縮#

HTTP/1.1 雖然可以用 gzip、brotli 對 body 做壓縮,但 header 完全無法壓縮,這在 HTTP/2 終於被處理。

多工(Multiplexing)#

HTTP/2 引入了 stream 與 frame 兩個觀念,並把訊息切成更小的二進位 frame 來傳輸:

  • Stream:類似一個串流管道,是一條邏輯通道。
  • Frame:在 stream 中流動的最小傳輸單位,每個 frame 都帶有所屬 stream 的識別碼。

有了 stream 識別碼,即使回應抵達的順序與請求送出的順序不同,client 仍能根據識別碼把 frame 重組回正確的請求,徹底解決了 HTTP pipelining 因順序而阻塞的問題。

多工為 HTTP/2 帶來的優點:

  • 可以並行送出多個請求,彼此互不影響。
  • 可以並行接收多個回應,彼此互不影響。
  • 在單一連線上同時處理多個 request 與 response。
  • 提升網路頻寬的使用效率。

Server Push#

Server Push 讓伺服器可以在 client 還沒解析完 HTML、還沒發出後續請求時,就主動把預期會用到的資源推送過去。

不過 Server Push 並沒有看起來那麼美好:

  • 開發者必須自己定義要推送哪些資源。
  • 多數 CDN 供應商目前不支援 Server Push,使用它就得放棄 CDN 的優勢。
  • Server Push 不會檢查資源是否已經被瀏覽器快取,可能會浪費頻寬重複推送。
  • 只能推送可被快取且沒有 response body 的資源,因此僅限 GET 與 HEAD 請求。

如果不確定 Server Push 是否值得使用,也可以考慮 resource hint(例如 prefetch),讓瀏覽器自行決定。

升級到 HTTP/2 後可省略的舊優化#

HTTP/2 的多工特性讓許多舊有的優化技巧(例如 CSS Sprites、inline scripting、Domain Sharding)變得不再必要——它們本來就是為了繞開 HTTP/1.x 的限制,現在限制消失了,那些 trade off 也不必再背了。

如何升級到 HTTP/2#

升級 HTTP/2 通常不需要改動現有程式碼,只要:

  • 確保使用 HTTPS 加密連線
  • Web Server 支援 HTTP/2

範例的 nginx 設定如下:

server {
    listen 443 ssl http2;
    server_name example.com;

    ssl_certificate     /etc/ssl/example.com.crt;
    ssl_certificate_key /etc/ssl/example.com.key;
}

如果不想自己處理 TLS 與 H2 設定,最簡單的方式是接上像 Cloudflare 這種 CDN,它會免費提供 TLS 1.3 憑證與 HTTP/2、HTTP/3 連線。

注意事項 透過 CDN 升級時,僅 client 到 CDN 邊緣節點這段走 HTTP/2 或 HTTP/3,CDN 到起源伺服器仍可能維持 HTTP/1.1。

對於原本不是全站 HTTPS 的網站,升級時還要特別注意 Mixed Content 問題:一旦頁面開始走 HTTPS,瀏覽器會擋掉殘留的 HTTP 資源,導致網站「看似掛掉」。

HTTP/3#

HTTP/2 仍未解決 TCP 層級的 Head-of-Line Blocking。為了徹底處理這個問題,HTTP/3(簡稱 H3)直接把傳輸層的 TCP 換掉,改用以 UDP(User Datagram Protocol)為基礎的 QUIC 協定。

UDP 本身是不可靠的傳輸,但 QUIC 在 UDP 之上重建了可靠傳輸所需的機制(例如重傳、流量控制、加密整合等),同時又避開了 TCP 一個封包遺失就阻塞整條連線的缺陷。

目前 HTTP/3 的支援度仍在擴展中,但已有不少網站開始採用,可預期未來會逐步普及。

小結#

HTTP 版本的升級是少數可以「無痛」帶來顯著效能提升的優化方式之一。回顧一下:

  • HTTP/1.1 受制於串行傳輸、連線數限制與 header 無壓縮,為前端帶來各種繞道優化。
  • HTTP/2 透過多工、二進位分幀、header 壓縮,解決了多數 HTTP 層級的 HOL 問題,也讓許多舊優化技巧變得多餘。
  • HTTP/3 改以 QUIC 取代 TCP,連 TCP 層級的 HOL 都一起處理掉。

身為一個常與 HTTP 共舞的前端開發者,理解這些觀念是十分重要的;遇到可以升級的場景,就大膽地升上去吧。

原文出處#