EP.10系統設計系列

CDN 與 Edge Computing:
讓資源在全球 50ms 內送達

CDN 原理、Edge Caching、Cache Invalidation、Cloudflare Workers — 大流量系統的靜態資源加速策略

Joseph Chen 2026 16 min read CDN · Edge · Cache Invalidation · Cloudflare

Section 1:沒有 CDN 的世界是什麼樣子?

在深入理解 CDN 之前,先想像一個沒有 CDN 的場景:你在高雄的電腦上,打開一個伺服器架在美國東岸的網站。 每一個請求——HTML、CSS、JavaScript、圖片——都需要跑完整的美洲太平洋路線。

沒有 CDN:台灣用戶訪問美國伺服器

text
台灣用戶(高雄)
    ↓ 發出 HTTP 請求
    ↓ 跨越太平洋光纖(物理延遲:約 60-80ms 單程)
美國東岸 Origin Server(紐約)
    ↓ 處理請求(伺服器計算時間)
    ↓ 回傳資源,再跑一次太平洋
台灣用戶(高雄)

TCP 單次往返(RTT):150-200ms
加上伺服器處理時間 + TCP 握手 + TLS 握手:
總延遲:300-500ms 以上

如果頁面有 20 個資源(HTML + CSS + JS + 圖片):
最壞情況:300ms × 20 = 6 秒才能載入完畢

有 CDN:資料近在咫尺

text
台灣用戶(高雄)
    ↓ DNS 解析:自動導向最近的 CDN 節點
    ↓ 連接台灣或亞洲的 CDN PoP(Point of Presence)
台灣 CDN PoP(延遲:5-20ms)
    ↓ 快取命中!直接從本地 SSD 讀取資源
台灣用戶(高雄)

RTT:10-40ms(縮短 10 倍以上)
頁面 20 個資源:因為 HTTP/2 多路複用,
快取命中的資源幾乎同時回傳

使用者感受:頁面「秒開」

CDN 的核心理念只有一句話:「把資料放到距離用戶最近的地方」。 這個看似簡單的想法,背後是全球數百個 PoP 節點的基礎建設,以及複雜的快取管理機制。

為什麼延遲很重要?數字說話

100ms

Amazon 研究:每 100ms 延遲損失 1% 銷售額

53%

Google 研究:行動用戶在 3 秒內未載入完成就會離開

10×

CDN 帶來的延遲改善倍數(跨洲際場景)


Section 2:CDN 如何工作?

CDN 的運作核心是「快取」。每個 CDN 的 PoP(Point of Presence,接入點)節點都有本地的儲存空間。 當用戶發出請求時,CDN 會先檢查本地是否有這份資源的副本,根據結果分三種情況處理。

三種快取狀態:Hit、Miss、Stale

text
第一個用戶(Cache Miss):
User → CDN PoP [找不到快取] → Origin Server [拿資源]
     → CDN PoP [儲存副本,設定過期時間]
     → User
延遲:完整的來回時間(包含到 Origin 的距離)

之後的用戶(Cache Hit):
User → CDN PoP [找到快取,直接回傳!] → User
延遲:只有用戶到 CDN PoP 的距離(最快)

快取過期後(Stale / Expired):
User → CDN PoP [快取存在但已過期]
     → Origin Server [重新驗證,詢問資源是否有變更]
     → 如果沒變更:304 Not Modified,CDN 更新過期時間,回傳快取內容
     → 如果有變更:200 OK,CDN 更新快取副本
     → User

HTTP Cache Headers:告訴 CDN 如何快取

CDN 不會自己決定要快取什麼、快取多久——一切都由 HTTP Response Headers 控制。 最重要的 Header 是 Cache-Control

text
# ── 場景 1:靜態資源(JS、CSS、圖片)── 永久快取
Cache-Control: public, max-age=31536000, immutable
# public    → CDN(共享快取)和瀏覽器都可以快取
# max-age   → 快取有效期(秒),31536000 秒 = 1 年
# immutable → 告訴瀏覽器「這個 URL 的資源永遠不會變」,不需要重新驗證
# 前提:URL 中帶有 content hash(如 app-a8f3c2d5.js),變更時換新 URL

# ── 場景 2:API 響應 ── 短暫快取 + 背景更新
Cache-Control: public, s-maxage=3600, stale-while-revalidate=86400
# s-maxage  → 只對共享快取(CDN)有效,優先級高於 max-age
# stale-while-revalidate → 快取過期後,最多可以繼續用舊快取 86400 秒,
#             同時在背景異步地去 Origin 更新快取
# 效果:用戶永遠看到快速回應,不會因為快取過期而感受到延遲

# ── 場景 3:登入後的個人化頁面 ── 只在瀏覽器快取
Cache-Control: private, no-cache
# private   → 只有用戶的瀏覽器可以快取,CDN 不能快取(因為每個人內容不同)
# no-cache  → 每次使用前都必須向 Origin 重新驗證(但可以複用快取的資料)

# ── 場景 4:支付頁面、敏感資料 ── 完全不快取
Cache-Control: no-store
# no-store  → 任何地方都不允許儲存,每次都重新從 Origin 取得

# ── 輔助 Header ──
ETag: "33a64df551425fcc55e4d42a148795d9f25f89d"
# ETag 是資源的「指紋」,用於重新驗證時判斷資源是否有變化
# 瀏覽器下次請求時帶上:If-None-Match: "33a64df551..."
# 如果資源沒變,Origin 回傳 304 Not Modified(省去傳輸資源體積)

Last-Modified: Thu, 08 May 2026 00:00:00 GMT
# 類似 ETag,但以時間作為驗證依據
# 精度較低(秒級),推薦優先使用 ETag

s-maxage vs max-age:max-age 對瀏覽器和 CDN 都有效,s-maxage(shared maxage)只對 CDN 等共享快取有效。 當兩個同時出現,CDN 優先使用 s-maxage。 這讓你可以給 CDN 更長的快取時間,同時控制瀏覽器更短的快取時間。


Section 3:Cache Invalidation — 快取更新的藝術

「電腦科學中有兩件真正難的事:命名,以及快取失效(Cache Invalidation)。」 — Phil Karlton(Netscape 工程師)

快取的問題在於:當你設定了 1 小時的快取,但 5 分鐘後你發現內容有錯誤需要更新,怎麼辦? 這就是快取失效(Cache Invalidation)要解決的問題。以下是三種主要策略。

策略一:Purge by URL(即時清除特定 URL)

最直接的方式:呼叫 CDN 的 API,強制刪除特定 URL 的快取副本。 下次有用戶請求時,CDN 會重新向 Origin 拉取最新內容。

bash
# Cloudflare Cache Purge API
curl -X POST   "https://api.cloudflare.com/client/v4/zones/{zone_id}/purge_cache"   -H "Authorization: Bearer {api_token}"   -H "Content-Type: application/json"   -d '{
    "files": [
      "https://example.com/articles/123",
      "https://example.com/articles/123/cover.jpg"
    ]
  }'

# 回應:
# { "success": true, "result": { "id": "9a7806061c..." } }
# 通常在幾秒內生效,全球 300+ 個節點同步清除

限制:你必須知道所有受影響的 URL。如果一篇文章同時出現在首頁、分類頁、標籤頁, 你需要清除所有相關的 URL,容易遺漏。

策略二:Cache-Busting(快取破壞)

不直接清除快取,而是改變 URL,讓 CDN 把它當作全新的資源來處理。 舊 URL 的快取會自然過期,新 URL 會建立新的快取。

html
<!-- 方式一:Query String 版本號(不推薦) -->
<!-- 有些 CDN 會忽略 Query String,不保證快取更新 -->
<link rel="stylesheet" href="/styles.css?v=2.1.0">
<script src="/app.js?v=a8f3c2d"></script>

<!-- 方式二:把 Content Hash 放在檔名中(推薦) -->
<!-- 每次檔案內容改變,hash 就不同,CDN 把它當新資源 -->
<!-- Next.js、Vite、Webpack 會在 Build 時自動做這件事 -->
<script src="/_next/static/chunks/app-a8f3c2d5.js"></script>
<link rel="stylesheet" href="/_next/static/css/3f8a9b2c.css">

<!-- 因為 URL 變了,可以安心設定 1 年的永久快取 -->
<!-- Cache-Control: public, max-age=31536000, immutable -->

優點:不需要呼叫 CDN API,完全由 Build 工具自動處理,零運維成本。
限制:只適合靜態資源(JS、CSS、圖片),不適合動態 API 響應(URL 固定)。

策略三:Surrogate Keys(Cache Tags)

最強大的策略:給快取的內容打「標籤(Tag)」,然後可以一次性清除所有帶某個標籤的快取。 Cloudflare、Fastly 都支援這個功能。

javascript
// ── 步驟 1:API 回應時加上 Cache-Tag Header ──
// app/api/articles/[id]/route.ts
export async function GET(request: Request, { params }: { params: { id: string } }) {
  const article = await db.article.findUnique({ where: { id: params.id } });

  return Response.json(article, {
    headers: {
      'Cache-Control': 'public, s-maxage=3600, stale-while-revalidate=86400',
      // 給這個響應打上標籤:文章 ID、作者 ID、分類 ID
      // Cloudflare 使用 Cache-Tag,Fastly 使用 Surrogate-Key
      'Cache-Tag': [
        `article:${params.id}`,
        `author:${article.authorId}`,
        `category:${article.categoryId}`,
      ].join(','),
    },
  });
}

// ── 步驟 2:當文章更新時,一次清除所有相關快取 ──
async function onArticleUpdate(articleId: string) {
  // 清除這篇文章的所有快取(文章頁、API 響應、可能出現在的任何地方)
  await fetch(
    `https://api.cloudflare.com/client/v4/zones/${process.env.CF_ZONE_ID}/purge_cache`,
    {
      method: 'POST',
      headers: {
        'Authorization': `Bearer ${process.env.CF_API_TOKEN}`,
        'Content-Type': 'application/json',
      },
      body: JSON.stringify({
        tags: [`article:${articleId}`],
      }),
    }
  );
  // 所有帶 article:123 標籤的快取(無論 URL 是什麼)都會被清除!
}

// ── 實際觸發時機 ──
// CMS webhook → API endpoint → onArticleUpdate()
// 或在 CMS 操作完成後,由 Next.js Server Action 呼叫

三種策略的選用時機:靜態資源(JS/CSS)優先用 Cache-Busting(Build 工具自動處理); API 響應且有明確 URL 時用 Purge by URL;API 響應且一個資源會影響多個 URL 時,用 Surrogate Keys。 大型系統通常三種策略並用。


Section 4:Edge Computing — 在 CDN 節點上執行程式碼

傳統 CDN 只能快取靜態資源:HTML、JS、CSS、圖片。 如果你需要執行邏輯(驗證、個人化、A/B 測試),請求就必須一路跑到 Origin Server。

Edge Computing 改變了這個模式:它讓你可以在 CDN 的 PoP 節點上直接執行 JavaScript, 在靠近用戶的地方完成邏輯處理,不需要再跑一趟 Origin。

text
傳統 CDN 架構(靜態快取):
                       ┌─────────────────────┐
Request → CDN PoP ──→ │  Origin Server       │
          ↑只能快取    │  (所有邏輯在這裡)   │
          靜態資源     └─────────────────────┘

Edge Computing 架構:
          ┌─────────────────────────────────────────┐
Request → │  CDN PoP(Edge Runtime)                │ ──→ Origin Server
          │  ✓ A/B Testing                           │     (只處理真正
          │  ✓ 認證驗證                              │      需要 DB 的請求)
          │  ✓ 安全 Headers                          │
          │  ✓ 地理重定向                             │
          │  ✓ 快取邏輯                              │
          └─────────────────────────────────────────┘
          延遲:10-20ms(就在用戶旁邊)

Cloudflare Workers 實際範例

Cloudflare Workers 是目前最成熟的 Edge Runtime,在全球 300+ 個節點上執行, 使用標準的 Web APIs(RequestResponsefetch)撰寫。

javascript
// wrangler.toml 部署設定(Cloudflare Workers)
// name = "my-edge-worker"
// main = "src/worker.js"
// compatibility_date = "2026-01-01"

// src/worker.js
export default {
  async fetch(request, env, ctx) {
    const url = new URL(request.url);

    // ── 使用案例 1:A/B Testing 在邊緣執行 ──
    // 不需要打到 Origin,直接在 CDN 節點決定版本
    if (url.pathname === '/') {
      // 讀取或設定 A/B 測試 Cookie
      const cookie = request.headers.get('Cookie') ?? '';
      let group = cookie.match(/ab_group=([AB])/)?.[1];

      if (!group) {
        // 新用戶:隨機分配組別
        group = Math.random() < 0.5 ? 'A' : 'B';
      }

      const targetUrl = group === 'A'
        ? new URL('/landing-v1', url.origin)
        : new URL('/landing-v2', url.origin);

      const response = await fetch(targetUrl.toString(), request);

      // 回傳結果,並設定 Cookie 記住組別
      const newResponse = new Response(response.body, response);
      if (!cookie.includes('ab_group=')) {
        newResponse.headers.append(
          'Set-Cookie',
          `ab_group=${group}; Path=/; Max-Age=2592000; SameSite=Lax`
        );
      }
      newResponse.headers.set('X-AB-Group', group);
      return newResponse;
    }

    // ── 使用案例 2:在邊緣加上安全 Headers ──
    // 不影響 Origin,在邊緣統一注入安全 Headers
    const response = await fetch(request);
    const secureResponse = new Response(response.body, {
      status: response.status,
      headers: new Headers(response.headers),
    });

    const securityHeaders = {
      'X-Frame-Options':           'DENY',
      'X-Content-Type-Options':    'nosniff',
      'X-XSS-Protection':          '1; mode=block',
      'Referrer-Policy':           'strict-origin-when-cross-origin',
      'Permissions-Policy':        'camera=(), microphone=(), geolocation=()',
      'Strict-Transport-Security': 'max-age=31536000; includeSubDomains',
    };

    Object.entries(securityHeaders).forEach(([key, value]) => {
      secureResponse.headers.set(key, value);
    });

    return secureResponse;
  },
};
javascript
// ── 使用案例 3:邊緣地理重定向 ──
export default {
  async fetch(request, env) {
    const country = request.cf?.country ?? 'US'; // Cloudflare 自動注入地理資訊
    const url = new URL(request.url);

    // 根據用戶所在地自動導向對應語系
    if (url.pathname === '/' && !url.searchParams.has('no-redirect')) {
      const langMap = {
        'TW': '/zh-TW',
        'JP': '/ja',
        'KR': '/ko',
        'US': '/en',
        'GB': '/en',
      };
      const targetLang = langMap[country] ?? '/en';
      return Response.redirect(`${url.origin}${targetLang}`, 302);
    }

    // ── 使用案例 4:邊緣 Rate Limiting(簡易版)──
    const clientIP = request.headers.get('CF-Connecting-IP') ?? 'unknown';
    const rateLimitKey = `rate:${clientIP}:${Math.floor(Date.now() / 60000)}`; // 每分鐘

    // 使用 Cloudflare KV 儲存計數器
    const current = parseInt(await env.KV.get(rateLimitKey) ?? '0');
    if (current >= 100) {
      return new Response('Too Many Requests', {
        status: 429,
        headers: { 'Retry-After': '60' },
      });
    }

    await env.KV.put(rateLimitKey, String(current + 1), { expirationTtl: 120 });

    return fetch(request);
  },
};

Section 5:Next.js 與 CDN 的最佳實踐

Next.js App Router 對 CDN 優化有完善的支援。理解這些設定可以讓你的應用在 Vercel、Cloudflare 或任何 CDN 上都能達到最佳效果。

next.config.ts 設定

typescript
// next.config.ts
import type { NextConfig } from 'next';

const nextConfig: NextConfig = {
  // ── 1. 自訂 Build ID(讓快取破壞可預測)──
  generateBuildId: async () => {
    // 使用 Git Commit Hash 作為 Build ID
    // 每次部署都有唯一 ID,靜態資源 URL 也會跟著變化
    return process.env.GIT_HASH ?? 'local-dev';
  },

  // ── 2. 指定 CDN 來源 ──
  // 靜態資源(JS、CSS、圖片)會從這個 CDN URL 提供
  // 而不是從你的 Origin Server
  assetPrefix: process.env.CDN_URL ?? '',
  // 例如:CDN_URL=https://cdn.example.com
  // 結果:/_next/static/chunks/app-abc123.js
  //       → https://cdn.example.com/_next/static/chunks/app-abc123.js

  // ── 3. 圖片最佳化設定 ──
  images: {
    // 允許的外部圖片 Domain
    remotePatterns: [
      { protocol: 'https', hostname: 'cdn.example.com' },
      { protocol: 'https', hostname: 'res.cloudinary.com' },
    ],
    // 支援的現代圖片格式(按優先級排序)
    formats: ['image/avif', 'image/webp'],
    // 圖片最小化快取時間(秒)
    minimumCacheTTL: 60 * 60 * 24 * 30, // 30 天
  },

  // ── 4. 自訂 HTTP Headers ──
  async headers() {
    return [
      {
        // 靜態資源:帶有 content hash 的 URL,永久快取
        source: '/_next/static/:path*',
        headers: [
          {
            key: 'Cache-Control',
            value: 'public, max-age=31536000, immutable',
          },
        ],
      },
      {
        // 圖片:1 個月快取
        source: '/images/:path*',
        headers: [
          {
            key: 'Cache-Control',
            value: 'public, max-age=2592000, stale-while-revalidate=86400',
          },
        ],
      },
    ];
  },
};

export default nextConfig;

API Route 的快取設定

typescript
// app/api/articles/route.ts
// Next.js App Router 的 Route Handler

export async function GET(request: Request) {
  const { searchParams } = new URL(request.url);
  const page = searchParams.get('page') ?? '1';

  const articles = await db.article.findMany({
    where: { published: true },
    orderBy: { createdAt: 'desc' },
    skip: (parseInt(page) - 1) * 10,
    take: 10,
  });

  return Response.json(articles, {
    headers: {
      // CDN 快取 1 小時,過期後可繼續用舊快取(最多 24 小時),同時背景更新
      'Cache-Control': 'public, s-maxage=3600, stale-while-revalidate=86400',
      // Surrogate Keys:方便批次失效
      'Cache-Tag': 'articles',
      // 加上 Vary:確保不同語系/地區的請求各自快取
      'Vary': 'Accept-Language',
    },
  });
}

// app/api/articles/[id]/route.ts
export async function GET(
  request: Request,
  { params }: { params: { id: string } }
) {
  const article = await db.article.findUnique({
    where: { id: params.id },
    include: { author: true, tags: true },
  });

  if (!article) {
    return Response.json({ error: 'Not Found' }, { status: 404 });
  }

  return Response.json(article, {
    headers: {
      'Cache-Control': 'public, s-maxage=3600, stale-while-revalidate=86400',
      'Cache-Tag': [`article:${params.id}`, `author:${article.authorId}`].join(','),
    },
  });
}

// 觸發快取失效:在文章更新後呼叫
// app/api/admin/articles/[id]/route.ts
export async function PATCH(
  request: Request,
  { params }: { params: { id: string } }
) {
  const data = await request.json();
  const updated = await db.article.update({
    where: { id: params.id },
    data,
  });

  // 文章更新後,清除 CDN 快取
  await fetch(
    `https://api.cloudflare.com/client/v4/zones/${process.env.CF_ZONE_ID}/purge_cache`,
    {
      method: 'POST',
      headers: { 'Authorization': `Bearer ${process.env.CF_API_TOKEN}` },
      body: JSON.stringify({ tags: [`article:${params.id}`] }),
    }
  );

  // 同時觸發 Next.js 的 On-Demand Revalidation
  revalidateTag(`article-${params.id}`);

  return Response.json(updated);
}

Section 6:CDN 的選型與架構決策

市場上主要的 CDN 提供商各有其定位。以下是三個最常見選項的比較,以及如何根據你的架構做選擇。

Cloudflare

最大全球網路 · 安全優先

優點

  • • 300+ PoP,覆蓋全球最廣
  • • 內建 DDoS 防護、WAF
  • • 免費方案可用(個人網站夠用)
  • • Cloudflare Workers:強大的 Edge Runtime
  • • Cache-Tag 支援,快取失效精準

考量

  • • 需要將 DNS 移至 Cloudflare 管理
  • • Enterprise 功能費用較高

Edge Runtime

Cloudflare Workers(全球最快)

適合

大多數網站,尤其需要安全保護時

AWS CloudFront

AWS 生態整合 · 企業首選

優點

  • • 與 S3、EC2、API Gateway 無縫整合
  • • Origin Shield:減少到 Origin 的請求
  • • 細緻的快取行為設定
  • • 支援 WebSocket、HTTP/3

考量

  • • 配置較複雜,學習曲線陡
  • • PoP 數量不如 Cloudflare(600+)
  • • 費用按流量計算,大流量較貴

Edge Runtime

Lambda@Edge / CloudFront Functions

適合

已在 AWS 生態的企業團隊

Vercel Edge Network

Next.js 深度整合 · 零配置

優點

  • • 與 Next.js App Router 深度整合
  • • 零配置:部署即自動啟用 CDN
  • • ISR(Incremental Static Regeneration)
  • revalidateTag() / revalidatePath()

考量

  • • 綁定 Vercel 平台(Vendor lock-in)
  • • Pro 方案後費用較高
  • • 自訂空間不如 Cloudflare

Edge Runtime

Vercel Edge Functions

適合

Next.js 應用,快速交付優先

CDN 快取命中率診斷

部署 CDN 後,如何確認快取是否真的在運作?用curl 查看 Response Headers 是最快的方式。

bash
# 用 curl 的 -I 參數只取 Response Headers(不下載 Body)
curl -I https://example.com/image.jpg

# ── Cloudflare 的 CDN 狀態 Header ──
# CF-Cache-Status: HIT      → 快取命中(Cloudflare 直接回傳)
# CF-Cache-Status: MISS     → 快取未命中(從 Origin 拉取)
# CF-Cache-Status: EXPIRED  → 快取已過期(重新驗證中)
# CF-Cache-Status: BYPASS   → 快取被跳過(有 Cookie 或私人請求)
# CF-Cache-Status: DYNAMIC  → 動態內容,不快取
# Age: 3600                 → 這份快取已存在 3600 秒(1 小時)

# ── AWS CloudFront 的 CDN 狀態 Header ──
# X-Cache: Hit from cloudfront   → 快取命中
# X-Cache: Miss from cloudfront  → 快取未命中
# X-Amz-Cf-Pop: NRT57-C2        → 你連接的 PoP(NRT = 東京)

# ── 完整診斷指令:重複跑兩次,確認快取命中 ──
for i in 1 2; do
  echo "Request $i:"
  curl -sI https://example.com/api/articles | grep -E "(CF-Cache-Status|Age|Cache-Control|X-Cache)"
  echo "---"
done

# 理想輸出:
# Request 1:
# CF-Cache-Status: MISS
# Cache-Control: public, s-maxage=3600, stale-while-revalidate=86400
# ---
# Request 2:
# CF-Cache-Status: HIT
# Age: 2
# Cache-Control: public, s-maxage=3600, stale-while-revalidate=86400
# ---

# ── 快取命中率計算 ──
# 目標:靜態資源命中率 > 99%,API 命中率 > 80%
# 可在 Cloudflare Dashboard 的 Analytics 頁面直接查看

CDN 效能優化 Checklist

  • ✓ 靜態資源(JS/CSS)使用 Content Hash 命名,設定永久快取
  • ✓ 圖片設定至少 30 天快取,啟用 WebP/AVIF 自動轉換
  • ✓ API 響應加上 Cache-Tag 便於精準失效
  • ✓ 個人化內容使用 Cache-Control: private
  • ✓ 定期用 curl 確認 CF-Cache-Status: HIT
  • ✓ 在 CDN Dashboard 監控快取命中率,目標 > 90%

CDNEdge ComputingCache InvalidationCloudflare WorkersWeb PerformanceCache-ControlNext.jsHTTP Headers
← EP.09 微服務
EP.11 →(Coming Soon)