FastAPI 課程 – 效能與最佳化
主題:gzip / brotli 壓縮
簡介
在現代 Web 應用中,傳輸效能往往決定了使用者的體驗好壞。即使後端服務本身已經非常快,若回傳的資料量過大,仍會因為網路瓶頸而產生延遲。
HTTP 壓縮(如 gzip、brotli)正是解決這個問題的利器:在伺服器端先把回應內容壓縮,客戶端再解壓縮,可降低 30%~70% 的傳輸大小,同時減少帶寬成本。
FastAPI 本身支援中介層(middleware)方式加入壓縮功能,且與 Starlette(FastAPI 的底層框架)緊密結合,使得開發者可以在幾行程式碼內完成設定。本篇文章將從概念說明、實作範例、常見陷阱與最佳實踐,帶你完整掌握在 FastAPI 中使用 gzip 與 brotli 壓縮的技巧。
核心概念
1. HTTP 壓縮的工作原理
- 客戶端 (Client) 在請求 Header 中加入
Accept-Encoding,告訴伺服器它支援哪些壓縮演算法,例如gzip, br(br 為 brotli)。 - 伺服器 (Server) 根據客戶端的需求與回應內容的類型,選擇合適的壓縮方式,將原始資料壓縮後放入回應 Header
Content-Encoding。 - 瀏覽器或其他客戶端 讀取
Content-Encoding,自動解壓縮後交給前端程式使用。
重點:壓縮只對可壓縮的內容有效(如 JSON、HTML、CSS),對已經壓縮過的二進位檔(如 JPEG、MP4)幾乎沒有效果,甚至會浪費 CPU。
2. gzip vs brotli
| 特性 | gzip | brotli |
|---|---|---|
| 壓縮效率 | 中等(約 20%~30% 減少) | 高(可達 30%~50%) |
| 解壓速度 | 快 | 稍慢,但仍在毫秒等級 |
| 瀏覽器支援 | 所有現代瀏覽器皆支援 | 大部分現代瀏覽器支援(IE 不支援) |
| 實作成本 | 標準庫即提供 (gzip 模組) |
需額外安裝 brotli 套件 |
在大多數情況下,先支援 gzip 已足以滿足需求;若想進一步縮減傳輸量,可在支援的瀏覽器上加上 brotli。
3. FastAPI 中的壓縮中介層
FastAPI 基於 Starlette,Starlette 已內建 GZipMiddleware,而 BrotliMiddleware 則需要額外安裝 starlette-brotli(或自行實作)。中介層的工作流程如下:
- 請求進入 FastAPI 前,先由 壓縮中介層 判斷是否需要壓縮回應。
- 若符合條件(如回應大小 >
minimum_size),則在回傳前自動壓縮。 - 客戶端收到壓縮後的資料,根據
Content-Encoding解壓。
程式碼範例
以下範例示範如何在 FastAPI 中加入 gzip 與 brotli 壓縮,並說明常見的客製化設定。
1️⃣ 基本的 GZipMiddleware
# main.py
from fastapi import FastAPI
from fastapi.responses import JSONResponse
from starlette.middleware.gzip import GZipMiddleware
app = FastAPI()
# 設定最小壓縮大小為 500 bytes,避免小回應被壓縮浪費 CPU
app.add_middleware(GZipMiddleware, minimum_size=500)
@app.get("/items")
async def get_items():
# 假設回傳大量資料
data = {"items": [{"id": i, "name": f"Item {i}"} for i in range(1, 1001)]}
return JSONResponse(content=data)
說明
minimum_size為可選參數,預設 500 bytes。- 當回應大小小於此值時,中介層會直接跳過壓縮,減少不必要的 CPU 開銷。
2️⃣ 加入 BrotliMiddleware(需安裝套件)
pip install brotli starlette-brotli
# main.py
from fastapi import FastAPI
from starlette.middleware.gzip import GZipMiddleware
from starlette_brotli import BrotliMiddleware # 注意套件名稱
app = FastAPI()
# 先加入 Brotli,讓支援的瀏覽器優先使用 brotli
app.add_middleware(
BrotliMiddleware,
minimum_size=500, # 與 GZip 相同的最小壓縮門檻
quality=5, # 壓縮等級 0~11,數字越高壓縮率越好,CPU 負擔也越大
)
# 再加入 GZip 作為備援(不支援 brotli 的客戶端會使用 gzip)
app.add_middleware(GZipMiddleware, minimum_size=500)
@app.get("/large-text")
async def large_text():
# 回傳一段長文字,方便觀察壓縮效果
text = "FastAPI " * 1000
return {"message": text}
說明
BrotliMiddleware必須放在GZipMiddleware前面,Starlette 會依照Accept-Encoding的優先順序選擇第一個可用的壓縮方式。quality控制壓縮等級,建議在 4~6 之間,兼顧效能與壓縮率。
3️⃣ 客製化回應類型的壓縮(只壓縮 JSON)
有時候我們只想對 JSON 回應做壓縮,而其他靜態檔案(如圖片)則交由 CDN 處理。可以透過自訂中介層實作:
# middleware.py
import gzip
from starlette.types import ASGIApp, Receive, Scope, Send
from starlette.responses import Response
class JsonGZipMiddleware:
def __init__(self, app: ASGIApp, minimum_size: int = 500):
self.app = app
self.minimum_size = minimum_size
async def __call__(self, scope: Scope, receive: Receive, send: Send):
if scope["type"] != "http":
await self.app(scope, receive, send)
return
async def send_wrapper(message):
if message["type"] == "http.response.start":
# 只在 JSON 回應時加上 header
headers = dict(message["headers"])
content_type = headers.get(b"content-type", b"").decode()
if "application/json" in content_type:
message["headers"].append((b"content-encoding", b"gzip"))
await send(message)
elif message["type"] == "http.response.body":
body = message.get("body", b"")
if len(body) >= self.minimum_size and b"gzip" in dict(message.get("headers", [])):
gz_body = gzip.compress(body)
message["body"] = gz_body
await send(message)
await self.app(scope, receive, send_wrapper)
# main.py
from fastapi import FastAPI
from middleware import JsonGZipMiddleware
app = FastAPI()
app.add_middleware(JsonGZipMiddleware, minimum_size=800)
@app.get("/stats")
async def stats():
# 模擬大量統計資料
return {"values": list(range(2000))}
說明
- 透過自訂中介層,我們可以精細控制哪些回應被壓縮。
- 範例中只在
application/json的回應加上Content-Encoding: gzip,避免對非 JSON 資料誤壓縮。
4️⃣ 使用測試工具驗證壓縮
# 直接用 curl 檢查 Header
curl -I -H "Accept-Encoding: gzip, br" http://localhost:8000/items
回應會顯示:
HTTP/1.1 200 OK
content-encoding: br
content-type: application/json
...
若只接受 gzip:
curl -I -H "Accept-Encoding: gzip" http://localhost:8000/items
則 content-encoding: gzip 會出現在回應 Header。
5️⃣ 在 Docker 中啟用壓縮的最佳配置
# Dockerfile
FROM python:3.12-slim
WORKDIR /app
COPY requirements.txt .
RUN pip install -r requirements.txt
COPY . .
EXPOSE 8000
# 設定環境變數,讓 uvicorn 使用多執行緒提升壓縮效能
ENV UVICORN_WORKERS=4
CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "8000", "--workers", "4"]
- 多執行緒(workers)能分散壓縮運算,避免單一執行緒成為瓶頸。
- 若部署在 Kubernetes,可配合 Horizontal Pod Autoscaler 讓 CPU 使用率保持在 70% 以下。
常見陷阱與最佳實踐
| 陷阱 | 說明 | 解決方式 |
|---|---|---|
| 過度壓縮小回應 | 對 200~300 bytes 的回應壓縮,解壓縮成本反而比傳輸成本高。 | 設定 minimum_size(建議 500~800 bytes)。 |
| 壓縮已壓縮的檔案 | 如 JPEG、PNG、MP4,gzip/brotli 只能減少極少量。 | 使用 StaticFiles 或 CDN,讓 CDN 處理靜態檔案的快取與壓縮。 |
| 忘記在 CORS 或 Proxy 前傳遞 Header | 反向代理(NGINX、Traefik)若自行壓縮,可能會把 Content-Encoding 兩次壓縮,導致客戶端解壓失敗。 |
在 Proxy 中 關閉自動壓縮(gzip off;),讓 FastAPI 完全負責壓縮。 |
| Brotli 只在 HTTPS | 部分瀏覽器僅在安全連線下使用 brotli。 | 若使用 HTTP,仍保留 gzip 作為備援。 |
| CPU 飽和 | 高壓縮等級(quality=9~11)在大量請求下會導致 CPU 飽和。 | 平衡 quality(4~6)或使用 硬體加速(如 Intel QuickAssist)。 |
最佳實踐
- 先啟用 gzip,觀察流量與 CPU 使用率。
- 逐步加入 brotli,僅在支援的客戶端上開啟。
- 設定
minimum_size,避免對小回應浪費資源。 - 使用 CDN 處理靜態資源的壓縮與快取,讓 API 僅專注於動態資料。
- 監控:在 Prometheus/Grafana 上觀測
http_response_size_bytes、process_cpu_seconds_total,確保壓縮不會成為瓶頸。
實際應用場景
| 場景 | 為何需要壓縮 | 建議設定 |
|---|---|---|
| 行動端 JSON API(如即時聊天、新聞推播) | 手機網路常受限於流量與延遲 | 啟用 BrotliMiddleware + GZipMiddleware,minimum_size=600,quality=5 |
| 大規模資料匯出(CSV、Excel) | 檔案可能超過 5 MB,直接下載會耗時 | 使用 StreamingResponse 搭配手動 gzip.compress,或將檔案先存於 S3,利用 S3 的內建壓縮下載 |
| 微服務內部呼叫 | 服務間頻繁傳遞大量 JSON,網路成本累積 | 在內部 Nginx 前端關閉壓縮,讓 FastAPI 直接壓縮,保持統一 Accept-Encoding |
| 多語系 HTML 網站 | 首頁載入大量文字與模板,需減少首次渲染時間 | 在 StaticFiles 前加入 BrotliMiddleware,確保 HTML、CSS、JS 都被壓縮 |
| IoT 裝置回傳感測資料 | 設備帶寬極低,且回傳頻率高 | 僅對 JSON 使用 gzip(quality=4),避免過度 CPU 負擔 |
總結
- gzip 與 brotli 是提升 HTTP 傳輸效能的關鍵工具,尤其在 JSON 為主的 API 中效果顯著。
- FastAPI 只需透過
add_middleware即可快速加入壓縮;若需要更高壓縮率,可同時掛載BrotliMiddleware作為備援。 - 設定最小壓縮大小、適當的壓縮等級,以及 避免在已壓縮的靜態資源上重複壓縮,是防止 CPU 飽和與資源浪費的核心要點。
- 在實務上,先啟用 gzip、觀測效能指標,再根據需求逐步導入 brotli,配合 CDN、反向代理的最佳化設定,能讓你的 FastAPI 服務在 效能、成本與使用者體驗 三方面取得平衡。
把握好這幾個設定,你的 FastAPI 應用就能在任何網路環境下,都保持快速回應與低流量消耗。