效能最佳化:讓 Workers 更快更省的完整攻略 | Cloudflare 完整教學

2026/09/16
效能最佳化:讓 Workers 更快更省的完整攻略 | Cloudflare 完整教學

這一篇要把上一篇設計好的架構,再榨出最後一分延遲成本——深入 Cloudflare Workers效能最佳化。我們會先從計費模型講起,搞懂為什麼要「減少 CPU time」;接著逐一實作各種利器:快取策略(Cache API 與邊緣快取)、Smart Placement(把 Worker 移到資料附近)、bindings 就近讀取、ctx.waitUntil 背景處理、串流回應避免阻塞、控制 bundle 大小與冷啟動;最後用 observability 觀測效果。目標:讓你的 Workers 又快又省。

前言

上一篇《架構模式與反模式》,我們拉高視角,把 KV、D1、Durable Objects、Service Bindings 這些零件組裝成一套能穩定擴展的架構觀——解決了「怎麼設計才穩」。這一篇要往下鑽:同樣一套架構,怎麼讓它跑得更快、花得更少

先給一句話定義:

效能最佳化(Performance Optimization)在 Cloudflare Workers 上,和傳統伺服器最大的不同,是「計費按 CPU 時間、不按牆鐘時間」——你等資料庫、等外部 API 的那段 I/O 時間,執行期不算你錢。所以 Workers 的效能與成本最佳化,核心不是「拚命省請求」,而是「少做 CPU 工作、聰明安排等待」:能並行的並行、能快取的快取、能背景的背景、能串流的串流,並在對的時候把 Worker 搬到離資料最近的地方。

打個比方會更好懂。想像你是一位餐廳外場,「效能」不是要你跑得多快,而是動線設計得多聰明。與其一次只端一道菜、來回跑三趟(序列 I/O),不如一次把三道菜一起端出(並行);與其讓客人乾等你去後廚拿早就準備好的醬料(每次回源),不如把常用醬料先放在手邊(快取);與其等整鍋湯煮好才上桌,不如先上前菜讓客人邊吃邊等(串流)。而 Smart Placement 就像是——如果你發現自己整天都在後廚跟外場之間跑,那乾脆把工作站搬到後廚旁邊。這一篇,就是教你設計 Workers 的「聰明動線」。

讀完這篇你會掌握:

  • 計費模型與 CPU time——為什麼「等待免費、運算才要錢」是所有最佳化的出發點。
  • 四大效能利器——快取(Cache API/邊緣快取)、Smart Placement、並行 I/O、串流回應,各自解決什麼延遲。
  • 背景處理與 bundle——ctx.waitUntil 把工作移到回應之後、控制 bundle 大小降低冷啟動。
  • 觀測效果——用 observability 與 Analytics Engine 量化你的最佳化,而不是憑感覺。

核心概念

一、效能的四個面向:CPU、延遲、快取、放置

在動手最佳化前,先建立一張全貌圖。Workers 的效能問題可以拆成四個面向,每個面向對應不同的最佳化手段:

面向瓶頸在哪計不計費主要武器
CPU timeJavaScript 運算(解析、迴圈、加密)計費減少同步運算、分批讓出、丟 Queues
延遲(I/O)等待子請求、DB、KV 回應不計費並行(Promise.all)、就近 bindings
快取重複回源、重複查詢省 CPU 也省延遲Cache API、邊緣快取、KV 快取
放置(Placement)Worker 離資料太遠影響延遲Smart Placement + Hyperdrive

這張表的關鍵洞見是:「等待」與「運算」要用完全不同的策略對付。等待(I/O)不計費,所以問題不是「等」本身,而是「排隊等」——把該並行的等待串成序列;運算(CPU)才計費,所以要盡量少做、做不掉的就分批讓出或丟給非同步佇列。搞混這兩者,你會把力氣花錯地方。

值得特別提醒的是,很多人從傳統伺服器帶來的直覺會在這裡失靈。在 Node.js 伺服器上,一個請求佔用一條執行緒,你會很在意「請求處理總時間」;但在 Workers 上,執行期同時管理著大量 isolate,你等待外部 API 的那 200ms 完全不佔用 CPU、也不計費——真正決定帳單的,是那段短短幾毫秒的 JavaScript 執行。所以一個實務上的量化目標是:盡量把每個請求的 CPU 時間壓在個位數毫秒。要做到這點,先問自己三個問題:這段運算能不能快取起來、下次直接拿?這些等待彼此獨立、能不能一起並行?這件事一定要在回應前做完,還是可以丟到 ctx.waitUntil 的背景?把這三個問題問過一輪,大部分效能問題都會自己浮現。

二、Smart Placement:把 Worker 移到資料附近

預設情況下,Workers 在最靠近使用者的邊緣節點執行。這對純邊緣運算(重寫標頭、A/B 分流、地理路由)是最快的。但當 Worker 需要頻繁存取位於特定地區的後端資料庫時,「靠近使用者」反而拖慢總延遲——來看這張示意:

【預設放置】Worker 在使用者附近,但資料庫在遠方
使用者(台北) --5ms--> Worker(台北節點) --150ms--> DB(美東)
                                        <--150ms--
若一個請求有 3 次 DB 來回:5 + 3×300 = 905ms

【Smart Placement】Worker 移到資料庫附近
使用者(台北) --150ms--> Worker(美東節點) --2ms--> DB(美東)
                                          <--2ms--
若一個請求有 3 次 DB 來回:150 + 3×4 + 150 = 312ms

**Smart Placement(智慧部署定位)**會分析 Worker 的流量模式,自動把它部署到「延遲最低」的位置——通常是靠近後端而非靠近使用者。運作機制:部署後最多 15 分鐘開始分析、預設以 1% 請求作為基準對比、追蹤 request duration 指標衡量效果。

該開的情況: 頻繁連外部 Postgres/MySQL、聚合特定區域後端 API、搭配 Hyperdrive 使用。 不該開的情況: 純靜態資源服務、沒有 fetch handler 的純 RPC Worker、流量不足(顯示 INSUFFICIENT_INVOCATIONS)。判準一句話:你的延遲瓶頸在「Worker↔後端」才開,在「使用者↔Worker」就別開。

三、幾個關鍵術語

  • CPU time(CPU 時間): Workers 的計費與限制單位,只計 JavaScript 實際執行的時間,不含等待 I/O。免費方案 10ms/請求,付費方案 30 秒/請求。
  • ctx.waitUntil(): 讓執行期「等待」回應送出後的背景工作完成(最多 30 秒),使用者無須等待,背景 Promise 也不會被靜默丟棄。
  • Cache API(caches.default): Worker 可程式化控制的邊緣快取,支援 match / put,搭配 ctx.waitUntil 非同步寫入。
  • 冷啟動(Cold Start): isolate 首次載入時的啟動延遲。Workers 的冷啟動極短(通常 <5ms),但過大的 bundle 會拉長它。

實作範例

理論看完,把幾個核心最佳化技巧實際寫出來,程式碼可直接作為專案起點。

1. 快取層:Cache API + 邊緣快取

第一個、也最有效的最佳化,是別讓每個請求都回源。用 Cache API 精細控制:先查邊緣快取,命中直接回、未命中才回源,再用 ctx.waitUntil 非同步寫入快取(不阻塞回應)。這一段和第 6 篇《快取策略與 Cache API》的觀念一脈相承,這裡聚焦在「怎麼用它降低 CPU 與延遲」:

// src/index.ts — Cache API 快取層:查→未命中→回源→背景寫入
export default {
  async fetch(request: Request, env: Env, ctx: ExecutionContext): Promise<Response> {
    const cache = caches.default;
    // 只快取 GET;用 URL 當快取鍵
    if (request.method !== 'GET') return fetch(request);

    const cacheKey = new Request(new URL(request.url).toString(), request);

    // 1. 先查邊緣快取(命中就零回源、零 CPU 解析)
    const cached = await cache.match(cacheKey);
    if (cached) {
      const hit = new Response(cached.body, cached);
      hit.headers.set('X-Cache', 'HIT');
      return hit;
    }

    // 2. 未命中,回源取得
    const response = await fetch(request);

    // 3. 只快取成功回應,並用 waitUntil 背景寫入——不讓使用者等寫快取
    if (response.ok) {
      const toCache = new Response(response.clone().body, response);
      toCache.headers.set('Cache-Control', 'public, max-age=300'); // 快取 5 分鐘
      ctx.waitUntil(cache.put(cacheKey, toCache));
    }

    const miss = new Response(response.body, response);
    miss.headers.set('X-Cache', 'MISS');
    return miss;
  },
} satisfies ExportedHandler<Env>;

若你是對「外部 API 的 fetch」做快取,還能更省事——直接用 cf 選項讓 Cloudflare 幫你快取:

// 用 fetch 的 cf 選項控制快取,連 Cache API 都不用手動操作
const response = await fetch('https://api.example.com/data', {
  cf: {
    cacheTtl: 300,          // 快取 5 分鐘
    cacheEverything: true,  // 即使來源沒給 Cache-Control 也快取
    cacheTtlByStatus: {     // 依狀態碼設定不同 TTL
      '200-299': 300,
      '404': 60,
      '500-599': 0,         // 不快取錯誤回應
    },
  },
});

2. 並行 I/O + 就近 bindings:少排隊、少等待

Workers 的延遲主要來自等待 I/O。同樣是等三個資料源,序列是總和、並行是最大值——這是零 CPU 成本就能砍掉一大段延遲的技巧:

// ❌ 序列:總延遲 = 50 + 60 + 40 = 150ms(白白排隊)
async function slow() {
  const user = await fetch('https://api.example.com/user');      // 50ms
  const orders = await fetch('https://api.example.com/orders');  // 60ms
  const stock = await fetch('https://api.example.com/inventory');// 40ms
  return { user, orders, stock };
}

// ✅ 並行:總延遲 = max(50, 60, 40) = 60ms
async function fast() {
  const [user, orders, stock] = await Promise.all([
    fetch('https://api.example.com/user'),
    fetch('https://api.example.com/orders'),
    fetch('https://api.example.com/inventory'),
  ]);
  return { user, orders, stock };
}

讀多個 KV 鍵也一樣——別用 for 迴圈逐一 await(N 次往返),用 Promise.all 一次並行(1 輪完成)。而且 KV、D1、R2 這些原生 bindings 是就近讀取的,延遲遠低於自己打 REST API:

// ❌ 序列讀 KV:N 個鍵 = N 次往返
async function readKvSlow(env: Env, keys: string[]) {
  const out = [];
  for (const k of keys) out.push(await env.MY_KV.get(k));
  return out;
}

// ✅ 並行讀 KV:一輪並行完成
async function readKvFast(env: Env, keys: string[]) {
  return Promise.all(keys.map((k) => env.MY_KV.get(k)));
}

3. ctx.waitUntil 背景處理 + 串流回應:避免阻塞

回應送出後才要做的事(記 log、發分析、寫快取),用 ctx.waitUntil 移到背景,使用者感知延遲直接砍掉那一段;同時它保證背景 Promise 不會因 isolate 被回收而被丟棄:

// ✅ 先回應使用者,分析事件在背景完成——不阻塞、不遺失
export default {
  async fetch(request: Request, env: Env, ctx: ExecutionContext): Promise<Response> {
    const response = Response.json({ ok: true });
    // 背景寫入分析,使用者不必等它完成(最多 30 秒)
    ctx.waitUntil(logToAnalytics(request, env));
    return response;
  },
} satisfies ExportedHandler<Env>;

async function logToAnalytics(request: Request, env: Env): Promise<void> {
  // 例如寫入 Analytics Engine 或第三方分析,細節見「觀測」一節
}

處理大檔案時,別把整包讀進記憶體——await response.text() 會一次吃掉可能超過 128MB 上限的內容,還拉高延遲。改用 TransformStream 串流,邊收邊送,第一個位元組更快到達(TTFB 大幅下降)、記憶體佔用是常數級:

// ❌ 緩衝整包:高延遲、可能爆記憶體
async function bufferAll(): Promise<Response> {
  const upstream = await fetch('https://files.example.com/big.csv');
  const text = await upstream.text();          // 整包進記憶體!
  return new Response(text.toUpperCase());
}

// ✅ 串流:邊收邊送,TTFB 更快、記憶體恆定
async function streamThrough(): Promise<Response> {
  const upstream = await fetch('https://files.example.com/big.csv');
  const { readable, writable } = new TransformStream();
  // 不 await pipeTo——讓串流在背景持續進行
  upstream.body!.pipeTo(writable);
  return new Response(readable, { headers: upstream.headers });
}

4. Smart Placement + Bundle 設定(wrangler.jsonc)

放置與冷啟動屬於「設定層」的最佳化。有頻繁 DB 存取就開 Smart Placement,並把 bundle 控制小以縮短冷啟動:

// wrangler.jsonc — 效能相關設定
{
  "name": "perf-worker",
  "main": "src/index.ts",
  "compatibility_date": "2026-08-01",
  "compatibility_flags": ["nodejs_compat"],

  // 有頻繁 DB 存取:把 Worker 移到最靠近資料庫的節點
  "placement": { "mode": "smart" },

  // 搭配 Hyperdrive,連線建立成本也一併消除
  "hyperdrive": [{ "binding": "DB", "id": "<HYPERDRIVE_ID>" }],

  // 開啟觀測,才能量化最佳化效果
  "observability": { "enabled": true, "head_sampling_rate": 1 }
}

Bundle 大小影響冷啟動與記憶體,上限 10MB(壓縮後)、建議 1MB 以下。用指令量測、逐步瘦身:

# 量測 bundle 大小
npx wrangler deploy --dry-run --outdir dist
ls -lh dist/

瘦身技巧:用 Workers 原生 API(cryptofetchURL)取代 npm 套件;用 nodejs_compat 旗標取代 polyfill;避免整包引入大型框架、只 import 需要的函式;偏好 tree-shaking 友善的 ESM 套件。

常見錯誤與最佳實踐

效能最佳化的另一半,是認得那些「悄悄拖慢或加錢」的坑。以下四個是 Workers 上最常見的效能陷阱。

陷阱一:在 Worker 裡做重度同步 CPU 運算(阻塞 + 超時 + 加錢)。

Workers 的 isolate 是單執行緒的,一段長時間的同步運算(大陣列排序、複雜正則、密集數學)會阻塞整個 isolate,不但拉高 CPU 帳單,還可能撞上 CPU 時間上限而被中斷。

// ❌ 反模式:對大陣列做重度同步運算,阻塞且可能超過 CPU 上限
const result = huge.reduce((acc, x) => {
  for (let i = 0; i < 1_000_000; i++) acc += Math.sqrt(x + i);
  return acc;
}, 0);

正確做法: 分批運算並用 await Promise.resolve() 讓出控制權;真正的重運算則丟給 Queues / Workflows 非同步處理,立即回 202 給使用者:

// ✅ 正解一:分批 + 讓出控制權,避免長時間阻塞
async function processInBatches(items: number[], size = 1000): Promise<number> {
  let sum = 0;
  for (let i = 0; i < items.length; i += size) {
    sum += items.slice(i, i + size).reduce((a, x) => a + Math.sqrt(x), 0);
    await Promise.resolve(); // 讓出控制權給其他 microtask
  }
  return sum;
}

// ✅ 正解二:重運算丟 Queue,立即回應,背景處理
export default {
  async fetch(request: Request, env: Env, ctx: ExecutionContext): Promise<Response> {
    const taskId = crypto.randomUUID();
    ctx.waitUntil(env.HEAVY_QUEUE.send({ taskId, payload: await request.json() }));
    return Response.json({ taskId, status: 'queued' }, { status: 202 });
  },
} satisfies ExportedHandler<Env>;

陷阱二:該快取的不快取,每個請求都回源。 重複的資料庫查詢或外部 API 回源,是最常見的延遲與 CPU 浪費。用 Cache API 或 KV 當快取層(cache-aside),命中就省下整段回源與 JSON 解析。記住寫快取要用 ctx.waitUntil 非同步進行,別讓使用者等你寫快取。

陷阱三:同步等待可以並行的 I/O。for 迴圈逐一 await 多個獨立的 fetch / KV 讀取,把該並行的等待排成序列,總延遲平白翻倍。凡是彼此獨立的 I/O,一律 Promise.all 並行——這是零成本的加速。

陷阱四:bundle 過大拖長冷啟動。 為了方便隨手 npm install 整包 moment、lodash、整個 AWS SDK,把 bundle 撐到好幾 MB,拉長冷啟動也吃記憶體。定期用 wrangler deploy --dry-run 量測,優先用原生 API 與 tree-shaking 友善套件。

最佳實踐小結:

  • 先減 CPU、再減延遲——計費按 CPU time,重運算分批讓出或丟 Queues;等待免費,但要並行別排隊。
  • 能快取就快取——Cache API / KV 當 cache-aside 層,寫入用 ctx.waitUntil 非同步進行。
  • 背景工作走 ctx.waitUntil——回應後的 log、分析、寫快取移到背景,砍掉使用者感知延遲。
  • 大回應用串流——TransformStream 取代 await response.text(),TTFB 更快、記憶體恆定。
  • 對的時候開 Smart Placement——頻繁碰後端 DB 才開,搭配 Hyperdrive 效果最佳。
  • 控制 bundle、量化效果——用原生 API 瘦身降低冷啟動,並開 observability 觀測。

小結

上一篇《架構模式與反模式》,我們把零件組裝成穩定的架構觀;這一篇《效能最佳化》,我們在同一套架構上,把延遲與成本再往下壓:

  • 計費模型與 CPU time——理解「等待免費、運算才要錢」,是所有最佳化的出發點。
  • 四大效能利器——快取(Cache API/邊緣快取)、並行 I/O 與就近 bindings、ctx.waitUntil 背景處理與串流回應、Smart Placement 把 Worker 移到資料附近。
  • 設定層最佳化——控制 bundle 大小降低冷啟動,並開啟 observability 量化每一項最佳化的效果。

一句話收束整篇:Workers 的效能最佳化,是「聰明安排等待、盡量少做運算」的藝術——因為計費按 CPU、等待不要錢。記住「先減 CPU、能快取就快取、能背景就背景、能串流就別緩衝、對的時候搬到資料旁」,你的 Worker 就能又快又省。

掌握了「怎麼跑得更快」,下一步就是「怎麼跑得更安全」。下一篇《安全最佳實踐》,我們會深入 Workers 的安全強化:Secrets 管理、timing-safe 密鑰比較、Turnstile 防機器人、Rate Limiting、安全標頭與 mTLS——把又快又省的應用,再打造得堅不可摧。我們下一篇見。

想查閱 Workers 效能與 Smart Placement 的官方最新指南,可以參考 Cloudflare Smart Placement 官方文件(各項功能名稱、限制與支援矩陣以當下官方文件為準)。

BenZ Software Developer

熱愛技術的軟體開發者,在這裡分享程式開發經驗與學習筆記。

本週主打

AI 自動化入門包

你每天手動在做的那些煩事,其實 AI 可以自己跑。這份給你 10 個照著做就會的自動化工作流 + 50 個複製即用的提示詞,不用會寫程式。

看看這個產品 →