Browser Run:託管的 headless 瀏覽器
第 29 章的結論是「需要完整 Linux 才用容器」。但有一類工作特別常見 —— 截圖、產 PDF、抓需要執行 JavaScript 才看得到內容的頁面 —— 常見到 Cloudflare 直接把它做成了託管服務。
你不需要自己包一個裝了 Chromium 的容器、不需要處理字型、不需要管理瀏覽器程序的生命週期。
30.1 改名了,但三件事沒改
Section titled “30.1 改名了,但三件事沒改”2026-04-15,Browser Rendering 改名為 Browser Run。 changelog 原文:
「We are renaming Browser Rendering to Browser Run. The name Browser Rendering never fully captured what the product does.」
文件路徑也搬到 /browser-run/。但實測確認,三個你會寫進程式碼的東西完全沒變:
| 項目 | 現況 |
|---|---|
| REST 路徑 | 仍然是 .../accounts/<id>/browser-rendering/content |
| wrangler 設定鍵 | 仍然是 "browser" |
| 套件名稱 | 仍然是 @cloudflare/puppeteer / @cloudflare/playwright |
唯一實際變了的是型別名稱。實測 wrangler types:
BROWSER: BrowserRun;舊教學裡的 BrowserWorker 或 Fetcher 已經換成 BrowserRun。搜尋文件時要注意這個時間點 —— 2026-04 之前的資料都用舊名,而舊的 /browser-rendering/ 文件路徑也還在,會同時出現在搜尋結果裡。
設定極簡:
{ "browser": { "binding": "BROWSER", "remote": true }}實測 config-schema.json,這個 binding 只有兩個欄位(binding 必填、remote 選填),additionalProperties: false。remote 那一個下一節就會用到。
30.2 兩條路:Quick Actions 與完整瀏覽器
Section titled “30.2 兩條路:Quick Actions 與完整瀏覽器”Quick Actions
Section titled “Quick Actions”十個預先做好的動作,你只給一個 URL:
| 動作 | 用途 | 狀態 |
|---|---|---|
/content | 執行 JS 之後的 HTML | GA |
/screenshot | 截圖 | GA |
/pdf | GA | |
/snapshot | HTML + base64 截圖 | GA |
/markdown | 頁面轉 Markdown | GA |
/scrape | 依 CSS selector 抓元素 | GA |
/json | 用 AI 抽出結構化資料 | GA |
/links | 抓出所有連結 | GA |
/accessibilityTree | 無障礙樹 | GA |
/crawl | 爬整個站 | Beta(唯一掛 beta 標籤的) |
2026-05 起可以直接用 binding 呼叫
Section titled “2026-05 起可以直接用 binding 呼叫”這是本章最實用的更新。changelog(2026-05-28):
「你現在可以用 browser binding 上的
quickAction()方法,直接從 Cloudflare Worker 呼叫 Browser Run Quick Actions。這簡化了 Worker 與 Browser Run 的互動方式,移除了對 API token 或外部 HTTP 請求的需求。」
const res = await env.BROWSER.quickAction("screenshot", { url: "https://example.com", viewport: { width: 1200, height: 630 }, screenshotOptions: { type: "png" },});需要 compatibility_date ≥ 2026-03-24。
binding 只涵蓋 8 個動作。 實測 worker-configuration.d.ts 裡 quickAction 的 overload 有且只有:
screenshot | pdf | content | scrape | links | snapshot | json | markdownaccessibilityTree 與 crawl 沒有 binding overload —— 這兩個只能走 REST API(配 API token)。
remote: true 是強制的
Section titled “remote: true 是強制的”官方原文:
「
.quickAction()方法尚不支援本機開發模式。用wrangler dev在本機開發時,你必須使用npx wrangler dev --remote,或在 browser binding 設定裡設"remote": true。」
實測不加 remote 時的行為,比文件描述的更值得注意。wrangler dev 的啟動表格顯示:
env.BROWSER Browser Run localbinding 存在,而且:
{ "ctorName": "Fetcher", "protoKeys": ["fetch", "connect", "constructor"], "hasQuickAction": "function", "quickActionSource": "[object JsRpcProperty]", "typeofNonsense": "function", "localCallResult": { "threwName": "TypeError", "threw": "The RPC receiver does not implement the method \"quickAction\"." }}又是第 23 章 Pipelines 那個 pattern。 binding 是一個 Fetcher,quickAction 是 [object JsRpcProperty],所以:
typeof env.BROWSER.quickAction === "function"是 truetypeof env.BROWSER.notARealMethod === "function"也是 true- 真的呼叫下去才丟
TypeError: The RPC receiver does not implement the method "quickAction".
feature detection 對這個 binding 完全無效。 這是本系列第五次遇到「存在但呼叫必失敗」的 API(第 6 章 Cache API stub、第 23 章 Pipelines、第 25 章
Astro.locals.runtime、第 27 章WebAssembly.compile、本章)。在 Workers 上,
typeof x === "function"什麼都證明不了。 記住這條。
加上 remote: true 之後,啟動表格變成 remote,wrangler 會印出 ⎔ Establishing remote connection...。
完整瀏覽器:Puppeteer / Playwright
Section titled “完整瀏覽器:Puppeteer / Playwright”Quick Action 做不到的(登入流程、多步互動、表單填寫),走完整瀏覽器:
import puppeteer from "@cloudflare/puppeteer";
const browser = await puppeteer.launch(env.BROWSER);const page = await browser.newPage();await page.goto(url, { waitUntil: "domcontentloaded" });Playwright 需要 compatibility_date ≥ 2025-09-15 加上 nodejs_compat,2025-09-25 起 GA。
30.3 成本控制(本章核心)
Section titled “30.3 成本控制(本章核心)”| Free | Paid | |
|---|---|---|
| 每日瀏覽器時間 | 10 分鐘 | 每月含 10 browser hours,之後 $0.09/hr |
| 並行瀏覽器 | 3 | 上限 120,但每月只含 10,之後 $2.00/個 |
| 新實例建立速率 | 每 20 秒 1 個 | 每秒 1 個 |
| Quick Actions 請求 | 每 10 秒 1 次 | 每秒 10 次 |
| 瀏覽器閒置 timeout | 60 秒 | 60 秒 |
⚠️ 並行數這一項,limits 頁與 pricing 頁講的是兩件事,很容易誤讀。 limits 頁寫「120 concurrent browsers per account」,pricing 頁寫「10 concurrent browsers included」。
120 是天花板,10 是免費額度。 你可以開到 120,但第 11 個開始每個每月 $2.00。並行數的計算方式是「你每日尖峰用量的月平均」。
還有一個計費上的重要區分:
Quick Actions 只計算 browser hours;Puppeteer / Playwright / CDP session 同時計算 browser hours 與 concurrent browsers。
也就是說 —— 能用 Quick Action 解決的事情,用 Quick Action 就好,它不吃並行額度。 這一句話可能是本章最省錢的一行。
另外一個小福利:「如果 Quick Actions 請求以 waitForTimeout 錯誤失敗,該 browser session 不計費。」
唯一的即時成本訊號:X-Browser-Ms-Used
Section titled “唯一的即時成本訊號:X-Browser-Ms-Used”實測 worker-configuration.d.ts 的 quickAction 文件註解,每一個 overload 都寫著:
Headers:
X-Browser-Ms-Used: Browser time consumed in milliseconds (set when status < 500)
這個 header 自 2025-09-02 起加入。它是你唯一能在每一個請求上量到花了多少錢的地方 —— dashboard 的統計是延遲的。
function cost(res: Response): number | null { const v = res.headers.get("x-browser-ms-used"); return v === null ? null : Number(v);}建議把它記進 Analytics Engine(第 22 章),維度用租戶 / 動作類型:
const res = await env.BROWSER.quickAction("screenshot", opts);env.AE.writeDataPoint({ blobs: ["screenshot", tenantId, new URL(opts.url).hostname], doubles: [1, cost(res) ?? 0], indexes: [tenantId],});這樣「哪個租戶、哪個動作、哪個目標網站在燒錢」變成一個 SQL 查詢,而不是一個月底的意外。
成本槓桿一:不要下載你不會用到的位元組
Section titled “成本槓桿一:不要下載你不會用到的位元組”BrowserRunBaseOptions 裡有一組請求過濾參數:
rejectResourceTypes?: BrowserRunResourceType[]; // 與 allowResourceTypes 互斥allowResourceTypes?: BrowserRunResourceType[];rejectRequestPattern?: string[]; // 與 allowRequestPattern 互斥allowRequestPattern?: string[];setJavaScriptEnabled?: boolean;抓文字內容時,圖片、影片、字型、追蹤腳本全都是浪費:
await env.BROWSER.quickAction("markdown", { url, rejectResourceTypes: ["image", "media", "font"], gotoOptions: { waitUntil: "domcontentloaded", timeout: 15_000 },});文件把這些參數描述成「Block specific resource types」,從來沒有把它們和成本連在一起。 但既然計費單位是 browser 時間,而下載與解析資源就是時間,這個推論很直接。範例專案的
/screenshot與/screenshot-unfiltered兩條路由就是設計來讓你自己量這個差異的 —— 對同一個 URL 打兩次,比較X-Browser-Ms-Used。
成本槓桿二:waitUntil 選對
Section titled “成本槓桿二:waitUntil 選對”gotoOptions.waitUntil 的預設是 "domcontentloaded"(型別註解裡寫明),timeout 預設 30000、最大 60000。
很多教學無腦寫 networkidle0,那會一直等到網路完全靜止 —— 對一個有輪詢或有 analytics beacon 的頁面,那就是等到 timeout。官方 FAQ 只在「JS 很重的頁面」這個情境下建議 networkidle2。
先用預設,量了 X-Browser-Ms-Used 之後再決定要不要調。
成本槓桿三:session 重用,而且要用對方法
Section titled “成本槓桿三:session 重用,而且要用對方法”冷啟一個瀏覽器是最貴的一步。官方 reuse-sessions 頁:
用
browser.disconnect()而不是browser.close()來讓瀏覽器保持存活,然後在下一個請求重新連上它。 「重用 session 消除了冷啟時間。」
const sessions = await puppeteer.sessions(env.BROWSER);const free = sessions.find((s) => !s.connectionId);
const browser = free ? await puppeteer.connect(env.BROWSER, free.sessionId) : await puppeteer.launch(env.BROWSER, { keep_alive: 600_000 });
// ... 做事 ...
browser.disconnect(); // ← 不是 close()keep_alive 的單位是毫秒,最大 600000(10 分鐘),而且它是「閒置 timeout,不是最大 session 時長」。預設的閒置 timeout 是 60 秒。
⚠️ Playwright 的行為和 Puppeteer 不一樣,官方明講:
「Playwright 處理
browser.close()的方式與 Puppeteer 不同。在 Playwright 裡,如果 browser 是用connect取得的,session 會中斷連線;如果是用launch取得的,session 會關閉。」所以在 Playwright 上,
close()的語意取決於你當初怎麼拿到那個 browser。這很容易寫出「以為在重用、其實每次都冷啟」的程式碼。
還有一個 FAQ 裡的實用建議:
「對大多數工作負載,重用既有的 browser session 並開一個新分頁,而不是為每個任務啟動新的瀏覽器。」分頁「不計入任何一個限制」。
分頁是免費的,瀏覽器不是。
關於「未關閉的 session 是失控帳單第一名」這個說法: 這是很多教學(包括本系列大綱初稿)的講法,但我在官方文件裡找不到這句警告。文件警告的其實是相反方向的風險 —— 閒置自動關閉:「如果瀏覽器閒置超過目前的限制,它會自動關閉,所以你必須有足夠的每分鐘請求數來維持它存活。」
兩件事都是真的(60 秒閒置就關掉,所以「洩漏」的窗口本來就有限),但本章只陳述有來源的那一句。
30.4 Puppeteer 還是 Playwright?
Section titled “30.4 Puppeteer 還是 Playwright?”Cloudflare 沒有推薦任何一個。 我確認過 FAQ 頁根本沒有討論這個問題(它談的是 429/422 錯誤、networkidle2、認證方式、session 重用、無痕隔離、資料保留期)。
overview 頁給的是依用途分流而不是依函式庫分流:
| 用途 | 官方建議 |
|---|---|
| 一般自動化、移植既有腳本 | Puppeteer、Playwright 或 CDP |
| 有韌性的爬取 | Stagehand(「AI 依意圖而非 selector 找元素」) |
| AI agent 瀏覽 | Playwright MCP 或 CDP + MCP client |
| 從 Workers 外部控制 | CDP |
以下是本章的編輯觀點,不是 Cloudflare 的建議:
如果你沒有既有的 Puppeteer 程式碼,我會選 Playwright —— 上游版本推進得比較快、
locatorAPI 的自動等待比 Puppeteer 的waitForSelector好寫得多、trace viewer 在除錯 headless 問題時價值很高。但如果你已經有一堆 Puppeteer 腳本要搬上來,就別動它。兩者在 Cloudflare 上都是 GA。
資料保留期(FAQ):crawl 結果 14 天,session 錄影 30 天。
30.5 LinkForge:每個短網址一張 OG 圖
Section titled “30.5 LinkForge:每個短網址一張 OG 圖”需求:分享短網址到社群時,顯示一張帶 slug 的預覽圖。
關鍵設計:用 html 而不是 url
Section titled “關鍵設計:用 html 而不是 url”BrowserRunCommonOptions 是一個聯集型別 —— url 與 html 剛好二選一:
type BrowserRunCommonOptions = | (BrowserRunBaseOptions & { url: string }) | (BrowserRunBaseOptions & { html: string });給 html 的話,瀏覽器不做任何導航、不發任何外部請求。這是最便宜的截圖形式:
const res = await env.BROWSER.quickAction("screenshot", { html: ogHtml(slug), // 純本地 HTML,零網路 viewport: { width: 1200, height: 630 }, screenshotOptions: { type: "png" },});一次生成,永久快取
Section titled “一次生成,永久快取”OG 圖對同一個 slug 永遠一樣,所以絕對不該每次請求都重新生成:
app.get("/og/:slug", async (c) => { const key = `og/${c.req.param("slug")}.png`;
const cached = await c.env.OG.get(key); // R2(第 12 章) if (cached) { return new Response(cached.body, { headers: { "content-type": "image/png", "cache-control": "public, max-age=31536000, immutable" }, }); }
const res = await c.env.BROWSER.quickAction("screenshot", { html: ogHtml(c.req.param("slug")), viewport: { width: 1200, height: 630 }, screenshotOptions: { type: "png" }, }); if (!res.ok) return c.notFound();
const png = await res.arrayBuffer(); c.executionCtx.waitUntil( c.env.OG.put(key, png, { httpMetadata: { contentType: "image/png" } }), ); return new Response(png, { headers: { "content-type": "image/png" } });});三層防護,每一層都對應前面的章節:
- R2 快取(第 12 章)—— 一個 slug 一輩子只付一次瀏覽器時間。
cache-control: immutable(第 6 章)—— Cloudflare 的邊緣快取接手,連 R2 都不用碰。waitUntil寫入(第 3 章)—— 使用者不用等 R2 寫完。
更好的做法:預先生成
Section titled “更好的做法:預先生成”甚至可以完全不在請求路徑上做 —— 短網址建立時就丟進 Queue(第 19 章):
// 建立短網址時await env.OG_QUEUE.send({ slug });
// consumer:批次生成,並且重用 sessionexport default { async queue(batch: MessageBatch<{ slug: string }>, env: Env) { const sessions = await puppeteer.sessions(env.BROWSER); const free = sessions.find((s) => !s.connectionId); const browser = free ? await puppeteer.connect(env.BROWSER, free.sessionId) : await puppeteer.launch(env.BROWSER, { keep_alive: 600_000 });
try { for (const msg of batch.messages) { const page = await browser.newPage(); // 分頁免費 await page.setViewport({ width: 1200, height: 630 }); await page.setContent(ogHtml(msg.body.slug)); const png = await page.screenshot({ type: "png" }); await env.OG.put(`og/${msg.body.slug}.png`, png); await page.close(); msg.ack(); } } finally { browser.disconnect(); // 不是 close() } },};一個 batch(第 19 章預設最多 10 則)共用一個瀏覽器、開 10 個分頁。相較於 10 次獨立的 Quick Action,這攤掉了 9 次冷啟。
30.6 本章實測結論彙整
Section titled “30.6 本章實測結論彙整”| # | 結論 | 影響 |
|---|---|---|
| 1 | 2026-04-15 改名 Browser Run,但 REST 路徑、wrangler 鍵 browser、套件名全部不變 | 只有型別名改了 |
| 2 | wrangler types 產生的型別現在是 BrowserRun(舊為 BrowserWorker / Fetcher) | 搜尋文件要注意 2026-04 這個分界 |
| 3 | binding schema 只有 binding 與 remote,additionalProperties: false | |
| 4 | 本機(無 remote)binding 存在且顯示 local,但 quickAction 是 [object JsRpcProperty] | |
| 5 | typeof env.BROWSER.quickAction === "function" 為 true;typeof env.BROWSER.notARealMethod 也是 true | feature detection 無效 |
| 6 | 實際呼叫丟 TypeError: The RPC receiver does not implement the method "quickAction". | 本系列第五次「存在但呼叫必失敗」 |
| 7 | quickAction 的型別 overload 只有 8 個動作 | accessibilityTree 與 crawl 沒有 binding 版,只能走 REST |
| 8 | binding 版 quick action 需要 compatibility_date ≥ 2026-03-24 | |
| 9 | 每個 quick action 回應都帶 X-Browser-Ms-Used(status < 500 時) | 唯一的即時成本訊號 |
| 10 | Paid 並行上限 120,但每月只含 10,之後 $2.00/個 | limits 頁與 pricing 頁講的是不同的東西 |
| 11 | Quick Actions 只算 browser hours;Puppeteer/Playwright/CDP 同時算 hours 與並行數 | 能用 Quick Action 就別開完整瀏覽器 |
| 12 | 閒置 timeout 預設 60 秒,keep_alive 單位毫秒、最大 600000 | 是閒置 timeout,不是最大時長 |
| 13 | 重用要用 disconnect() 不是 close() | 官方 reuse-sessions 頁明載 |
| 14 | Playwright 的 close() 語意取決於當初是 connect 還是 launch | 很容易寫出假的重用 |
| 15 | 分頁「不計入任何一個限制」 | 分頁免費,瀏覽器不免費 |
| 16 | gotoOptions.waitUntil 預設 "domcontentloaded",timeout 預設 30000 / 最大 60000 | 不要無腦寫 networkidle0 |
| 17 | rejectResourceTypes / allowRequestPattern 有文件,但沒有被描述成省錢手段 | 成本推論是本章自己做的 |
| 18 | url 與 html 是聯集型別,剛好二選一 | 用 html 是最便宜的截圖 |
| 19 | waitForTimeout 失敗的 Quick Action 不計費 | |
| 20 | /crawl 是唯一掛 Beta 的動作;Free 每日 5 次、每次最多 100 頁 | |
| 21 | Cloudflare 沒有在 Puppeteer 與 Playwright 之間選邊;FAQ 完全沒提這題 | 依用途分流,不依函式庫 |
| 22 | 「未關閉的 session 是失控帳單第一名」找不到官方來源;文件警告的是相反的閒置自動關閉 | 本章不引用不存在的警告 |
| 23 | 資料保留:crawl 結果 14 天、session 錄影 30 天 |
30.7 動手練習
Section titled “30.7 動手練習”- 對同一個內容豐富的網站打
/screenshot與/screenshot-unfiltered,比較X-Browser-Ms-Used。這是 30.3 那個「文件沒說但推論很直接」的成本主張的實證。 - 把
X-Browser-Ms-Used寫進 Analytics Engine(第 22 章),做一個「每租戶每日瀏覽器成本」的查詢。 - 用 Playwright 寫兩版重用:一版
launch()後close(),一版connect()後close(),用puppeteer.sessions()觀察 session 是否還在 —— 驗證 30.3 那個語意差異。 - 對一個有輪詢的頁面分別用
domcontentloaded、networkidle2、networkidle0,比較X-Browser-Ms-Used與成功率。 - 實作 30.5 的 Queue 版 OG 生成,比較「一個 batch 一個瀏覽器開 10 分頁」與「10 次獨立 Quick Action」的總 browser time。
- Browser Run · 改名 changelog(2026-04-15)
- Limits · Pricing · FAQ
- Quick Actions ·
/contentendpoint · binding 版 quickAction changelog(2026-05-28) - Puppeteer · Playwright · Reuse sessions
- API 表面來源:
wrangler types產生的worker-configuration.d.ts(BrowserRun及其quickActionoverload 與選項型別)
下一章(第 31 章):Images 與媒體交付 —— 當你要處理的不是「渲染一個網頁」而是「大量既有圖片的轉檔、裁切與交付」時,Images binding 與 Media Transformations 才是對的工具。