- Published on
Express 要不要加 Compression?從 localhost Lighthouse 到 Cloudflare 架構評估
- Authors

- Name
- Harry
Express 要不要加 Compression?從 localhost Lighthouse 到 Cloudflare 架構評估
完成首頁 CLS 與 WebP/LCP 優化後,我在本機 production build 嘗試替 Express 加上 HTTP response compression。Lighthouse 的總傳輸量從 3,077 KiB 降到 1,572 KiB,Performance 從 75 上升到 86,看起來是很直接的改善。
但進一步檢查正式環境後,結論改變了:使用者收到的 HTML、CSS 與 JavaScript 原本就已由 Cloudflare 以 Brotli 傳輸。把 compression middleware 加進 Express,主要影響的是 origin 到 Cloudflare 的傳輸與未命中快取的 request,卻會增加 Node.js 即時壓縮的 CPU 成本。
因此這次的最後決定是:保留實驗與驗證紀錄,但從 PR 移除 Express compression。 在缺少 origin response time、Cloudflare cache hit ratio 與 pod CPU 等資料前,不能只根據 localhost Lighthouse 分數決定 production 架構。
這種優化叫什麼?
最精確的名稱是 HTTP response compression(HTTP 回應壓縮),也可以稱為 Brotli/Gzip 傳輸壓縮或 network payload optimization。
它和以下幾種優化不同:
- Minification:移除不必要的空白與註解,並將可安全改名的識別字縮短(mangle)。
- Tree shaking:從 bundle 移除未使用的 export。
- 圖片壓縮:改變 PNG、JPEG、WebP 或 AVIF 的編碼方式。
- HTTP response compression:不改變解壓後內容,只減少網路上傳送的 bytes。
例如瀏覽器最後仍會執行同一份 JavaScript,但網路上實際傳輸的資料可以小很多。這能改善下載時間,卻不會降低 JavaScript parse、compile、evaluation 或 React hydration 的成本。
本機實驗怎麼做?
原本的 Express server 直接提供靜態檔案:
const server = Express()
server.disable('x-powered-by').use(Express.static(process.env.RAZZLE_PUBLIC_DIR, { maxAge: '14d' }))
實驗版本安裝 compression,並把 middleware 放在 Express.static 和 SSR route 前面:
import compression from 'compression'
import Express from 'express'
const server = Express()
server
.disable('x-powered-by')
.use(compression())
.use(Express.static(process.env.RAZZLE_PUBLIC_DIR, { maxAge: '14d' }))
瀏覽器會在 request 帶上支援的編碼:
Accept-Encoding: gzip, deflate, br
Server 選擇 Brotli 時,response 會包含:
Content-Encoding: br
Vary: Accept-Encoding
Vary: Accept-Encoding 告訴 cache:同一個 URL 可能有不同編碼版本,不能把 Brotli response 交給不支援 Brotli 的 client。
如何正確驗證?
用 Chrome DevTools Network
- 開啟 Network 面板並勾選
Disable cache。 - 重新整理頁面。
- 分別選擇首頁 Document、CSS bundle 和 JavaScript bundle。
- 在
Headers→Response Headers檢查Content-Encoding和Vary。
本機實驗中的 CSS response 顯示:
Content-Encoding: br

這張圖只證明該 CSS response 使用 Brotli,不能單獨證明所有資源都有壓縮,也不能證明 production 必須由 Express 壓縮。
用 curl 驗證
Production build 的檔名包含 content hash,不能直接假設路徑是 /static/css/bundle.css。先從 build manifest 取得實際路徑:
CSS_PATH=$(node -p "require('./build/assets.json').client.css")
curl -sS -D - -o /dev/null \
-H 'Accept-Encoding: br' \
"http://localhost:3000${CSS_PATH}"
預期同時看到正確的 status、content type 與 encoding:
HTTP/1.1 200 OK
Content-Type: text/css; charset=UTF-8
Vary: Accept-Encoding
Content-Encoding: br
如果路徑不存在,Express.static 可能把 request 交給 SSR fallback。這時即使看到 Content-Encoding: br,只要 Content-Type 是 text/html,驗證到的仍是 HTML,不是 CSS。
再用 GET request 測試 Gzip:
curl -sS -D - -o /dev/null \
-H 'Accept-Encoding: gzip' \
http://localhost:3000/
不要改用 curl -I。-I 送出的是 HEAD request,而 compression@1.8.2 會跳過 HEAD;沒有 Content-Encoding 不代表壓縮設定失敗。-D - -o /dev/null 仍送出 GET,只是不把 body 顯示在 terminal。
localhost Lighthouse 觀察到了什麼?
兩次測試都使用本機 production build 與 Lighthouse Mobile simulated throttling:
| 指標 | WebP/preload 後 | Express compression 實驗 | 變化 |
|---|---|---|---|
| Performance | 75 | 86 | +11 |
| First Contentful Paint | 3.9 s | 1.9 s | -2.0 s |
| Largest Contentful Paint | 3.9 s | 1.9 s | -2.0 s |
| Speed Index | 5.7 s | 3.2 s | -2.5 s |
| Total Blocking Time | 0 ms | 450 ms | +450 ms |
| Cumulative Layout Shift | 0 | 0 | 不變 |
| Total payload | 3,077 KiB | 1,572 KiB | 約 -49% |
主要文字資源的 transfer size 也明顯下降:
| 資源 | 壓縮前 | 壓縮後 | 約減少 |
|---|---|---|---|
| HTML | 39,989 bytes | 9,880 bytes | 75% |
| CSS bundle | 201,380 bytes | 32,771 bytes | 84% |
| JavaScript bundle | 1,275,144 bytes | 344,378 bytes | 73% |
| 圖表 chunk | 518,083 bytes | 130,053 bytes | 75% |
這證明 middleware 在本機確實減少了 Express 到瀏覽器的傳輸量。然而 localhost 的連線拓撲是:
browser → Express
正式環境則是:
browser → Cloudflare → Express origin
兩者不是同一個測試條件,因此本機減少 49% 不能直接當成正式環境使用者也會獲得同等改善。
為什麼 TBT 從 0 變成 450 ms?
這不是 Brotli 解壓縮讓主執行緒多做了 450 ms。TBT 只計算 FCP 到 TTI 之間,每個 main-thread task 超過 50 ms 的部分:
blocking time = max(task duration - 50 ms, 0)
壓縮前,主 bundle 與圖表 chunk 下載較慢,相關 long tasks 落在 Lighthouse 的 TBT 計算區間之外。壓縮後,JavaScript 更早下載完成並開始執行,原本被慢速下載藏在後面的 CPU 成本因此進入計算區間。
本次報告中總 main-thread work 反而從 2.2 s 降到 1.7 s,所以不能把 TBT 上升直接解讀成 Brotli 解壓縮造成的 regression。不過單次 Lighthouse 數字仍會受 CPU 排程、背景程式和第三方 script 影響,應重複測試並取中位數。
為什麼最後移除 Express compression?
正式站的 DevTools 與 response header 已顯示,使用者收到的 HTML、CSS 與 JavaScript 原本就有:
Content-Encoding: br
也就是 Cloudflare 已負責 browser-facing compression。此時再把 middleware 放進 Express,可能只在 Cloudflare 回源或 cache miss 時減少 origin 傳輸量,對一般使用者不一定帶來本機 Lighthouse 所顯示的改善。
PR review 的外部測量指出,SSR HTML 未壓縮約 31–50 KB、Brotli 後約 7–12 KB,每次未快取頁面約節省 25–40 KB 的 origin 傳輸。這個節省是否值得 Node.js 的即時壓縮 CPU 成本,必須配合實際流量與監控資料評估,不能只看檔案縮小比例。
Express 官方文件也建議在 production 由 reverse proxy 層處理 compression。對 CSS、JavaScript 這類不隨 request 改變的資源,還可以在 build 或 CDN 階段預先壓縮,避免每次 request 都消耗應用程式 CPU。
因此要重新評估應用層壓縮,至少需要:
- Cloudflare cache hit ratio 與回源 request 數量
- origin response time/TTFB
- Node.js pod CPU 使用率與尖峰負載
- 壓縮前後的 origin egress 差異
- CDN 是否已能完成 Brotli/Gzip negotiation 與 cache
在這些資料齊全前,移除 middleware 是較符合目前架構的選擇。
Brotli quality 4 與 11 的取捨
compression@1.8.2 的即時 Brotli 預設 quality 是 4,不是最高的 11。這是 response time、CPU 成本與檔案大小之間的折衷;即時壓縮直接使用 quality 11,可能以較高的 server CPU 與延遲換取更小的 response。
預先壓縮的靜態檔案不佔用每次 request 的 CPU budget,因此可以考慮 quality 11,再由 CDN 根據 Accept-Encoding 回傳相對應的版本。動態 SSR HTML 是否需要在 origin 即時壓縮,則應由上述 production 指標決定。
結論
- HTTP response compression 確實能減少 HTML、CSS 與 JavaScript 的傳輸 bytes。
- 本機實驗的 payload 從
3,077 KiB降到1,572 KiB,只能證明 middleware 生效,不能直接代表 production 使用者收益。 - Production 已由 Cloudflare 對瀏覽器提供 Brotli;Express compression 的主要潛在收益改成 origin 到 Cloudflare 的傳輸。
- 即時壓縮有 Node.js CPU 成本,必須與 cache、origin latency、egress 和 pod CPU 一起衡量。
- 因此這次從 PR 移除 Express compression,保留已驗證的 CLS 與 WebP/LCP 優化。