本文 AI 產出,尚未審核

FastAPI 課程 – 效能與最佳化

主題:gzip / brotli 壓縮


簡介

在現代 Web 應用中,傳輸效能往往決定了使用者的體驗好壞。即使後端服務本身已經非常快,若回傳的資料量過大,仍會因為網路瓶頸而產生延遲。
HTTP 壓縮(如 gzip、brotli)正是解決這個問題的利器:在伺服器端先把回應內容壓縮,客戶端再解壓縮,可降低 30%~70% 的傳輸大小,同時減少帶寬成本。

FastAPI 本身支援中介層(middleware)方式加入壓縮功能,且與 Starlette(FastAPI 的底層框架)緊密結合,使得開發者可以在幾行程式碼內完成設定。本篇文章將從概念說明、實作範例、常見陷阱與最佳實踐,帶你完整掌握在 FastAPI 中使用 gzip 與 brotli 壓縮的技巧。


核心概念

1. HTTP 壓縮的工作原理

  1. 客戶端 (Client) 在請求 Header 中加入 Accept-Encoding,告訴伺服器它支援哪些壓縮演算法,例如 gzip, br(br 為 brotli)。
  2. 伺服器 (Server) 根據客戶端的需求與回應內容的類型,選擇合適的壓縮方式,將原始資料壓縮後放入回應 Header Content-Encoding
  3. 瀏覽器或其他客戶端 讀取 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(或自行實作)。中介層的工作流程如下:

  1. 請求進入 FastAPI 前,先由 壓縮中介層 判斷是否需要壓縮回應。
  2. 若符合條件(如回應大小 > minimum_size),則在回傳前自動壓縮。
  3. 客戶端收到壓縮後的資料,根據 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)

最佳實踐

  1. 先啟用 gzip,觀察流量與 CPU 使用率。
  2. 逐步加入 brotli,僅在支援的客戶端上開啟。
  3. 設定 minimum_size,避免對小回應浪費資源。
  4. 使用 CDN 處理靜態資源的壓縮與快取,讓 API 僅專注於動態資料。
  5. 監控:在 Prometheus/Grafana 上觀測 http_response_size_bytesprocess_cpu_seconds_total,確保壓縮不會成為瓶頸。

實際應用場景

場景 為何需要壓縮 建議設定
行動端 JSON API(如即時聊天、新聞推播) 手機網路常受限於流量與延遲 啟用 BrotliMiddleware + GZipMiddlewareminimum_size=600quality=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 應用就能在任何網路環境下,都保持快速回應與低流量消耗。