mcpField
C
cloudflare-browser-rendering

cloudflare-browser-rendering

github.com/jackspace云与 DevOps1643 次安装12 周前更新
代码仓库

关于 cloudflare-browser-rendering

面向开发者的 Cloudflare Browser Rendering 参考技能,介绍如何通过 Workers Bindings 使用 Puppeteer 或 Playwright 自动化浏览器,涵盖页面捕获、PDF、抓取和测试,并说明会话管理。

主要功能

  1. 01Puppeteer 与 Playwright API 指南
  2. 02浏览器会话复用与监控
  3. 03页面截图与 PDF 生成
  4. 04动态页面导航与内容提取
  5. 05Workers Binding 配置指南

适用场景

  1. 01捕获网页截图或 PDF
  2. 02抓取动态网站内容
  3. 03自动化前端测试
  4. 04在多个 Worker 间复用浏览器会话

安装

复制下面的命令,在终端中运行。

$npx skills add https://github.com/jackspace/claudeskillz --skill cloudflare-browser-rendering

安装后在 Claude Code 中输入/cloudflare-browser-rendering确认已加载。

附带文件README.mdSKILL.json

详细介绍

Cloudflare Browser Rendering - 完整参考

用于构建 Cloudflare Browser Rendering 浏览器自动化工作流的生产就绪知识域。

状态:生产就绪 ✅ 上次更新:2025-10-22 依赖项:cloudflare-worker-base(用于 Worker 设置) 最新版本:@cloudflare/[email protected]、@cloudflare/[email protected]、[email protected]


目录

  1. 快速入门(5 分钟)
  2. Browser Rendering 概述
  3. Puppeteer API 参考
  4. Playwright API 参考
  5. 会话管理
  6. 常见模式
  7. 定价与限制
  8. 已知问题预防
  9. 生产检查清单

快速入门(5 分钟)

1. 添加 Browser 绑定

wrangler.jsonc:

{
  "name": "browser-worker",
  "main": "src/index.ts",
  "compatibility_date": "2023-03-14",
  "compatibility_flags": ["nodejs_compat"],
  "browser": {
    "binding": "MYBROWSER"
  }
}

为什么需要 nodejs_compat? Browser Rendering 需要 Node.js API 和 polyfill。

2. 安装 Puppeteer
npm install @cloudflare/puppeteer
3. 截取第一张屏幕截图
import puppeteer from "@cloudflare/puppeteer";

interface Env {
  MYBROWSER: Fetcher;
}

export default {
  async fetch(request: Request, env: Env): Promise<Response> {
    const { searchParams } = new URL(request.url);
    const url = searchParams.get("url") || "https://example.com";

    // Launch browser
    const browser = await puppeteer.launch(env.MYBROWSER);
    const page = await browser.newPage();

    // Navigate and capture
    await page.goto(url);
    const screenshot = await page.screenshot();

    // Clean up
    await browser.close();

    return new Response(screenshot, {
      headers: { "content-type": "image/png" }
    });
  }
};
4. 部署
npx wrangler deploy

在以下地址测试:https://your-worker.workers.dev/?url=https://example.com

重要:

  • 始终将 env.MYBROWSER 传给 puppeteer.launch()(不能是 undefined)
  • 完成后始终调用 browser.close()(或使用 browser.disconnect() 以便重用会话)
  • 使用 nodejs_compat 兼容性标志

Browser Rendering 概述

什么是 Browser Rendering?

Cloudflare Browser Rendering 提供运行在 Cloudflare 全球网络上的无头 Chromium 浏览器。使用 Puppeteer 和 Playwright 等熟悉的工具自动执行浏览器任务:

  • 屏幕截图 - 捕获网页的可视化快照
  • PDF 生成 - 将 HTML/URL 转换为 PDF
  • 网页抓取 - 从动态网站提取内容
  • 测试 - 自动执行前端测试
  • 爬取 - 浏览多页工作流
两种集成方式
方式适用场景复杂度
Workers 绑定复杂自动化、自定义工作流、会话管理高级
REST API简单的屏幕截图/PDF 任务简单

本技能介绍 Workers 绑定(提供完整 Puppeteer/Playwright API 的高级方式)。

Puppeteer 与 Playwright
功能PuppeteerPlaywright
API 熟悉度最受欢迎使用人数不断增长
包@cloudflare/[email protected]@cloudflare/[email protected]
会话管理✅ 高级 API⚠️ 基础
浏览器支持仅 Chromium仅 Chromium(暂不支持 Firefox/Safari)
适用场景屏幕截图、PDF、抓取测试、前端自动化

建议:大多数场景使用 Puppeteer。如果你已将 Playwright 用于测试,则它是理想选择。


Puppeteer API 参考

puppeteer.launch()

启动新的浏览器实例。

签名:

await puppeteer.launch(binding: Fetcher, options?: LaunchOptions): Promise<Browser>

参数:

  • binding(必需)- 来自 env.MYBROWSER 的浏览器绑定
  • options(可选):
    • keep_alive(数字)- 让浏览器保持运行 N 毫秒(最大值:600000 = 10 分钟)

返回值: Promise<Browser> - 浏览器实例

示例:

const browser = await puppeteer.launch(env.MYBROWSER, {
  keep_alive: 60000 // Keep alive for 60 seconds
});

重要: 必须传入 env.MYBROWSER 绑定。出现错误“Cannot read properties of undefined (reading 'fetch')”表示未传入该绑定。


puppeteer.connect()

连接到现有浏览器会话。

签名:

await puppeteer.connect(binding: Fetcher, sessionId: string): Promise<Browser>

使用场景:

  • 重用现有浏览器会话以提升性能
  • 在多个 Worker 之间共享浏览器实例
  • 缩短启动时间

示例:

const sessionId = "478f4d7d-e943-40f6-a414-837d3736a1dc";
const browser = await puppeteer.connect(env.MYBROWSER, sessionId);

puppeteer.sessions()

列出当前运行的浏览器会话。

签名:

await puppeteer.sessions(binding: Fetcher): Promise<SessionInfo[]>

返回值:

interface SessionInfo {
  sessionId: string;
  startTime: number;
  connectionId?: string;  // Present if worker is connected
  connectionStartTime?: number;
}

示例:

const sessions = await puppeteer.sessions(env.MYBROWSER);
// Find sessions without active connections
const freeSessions = sessions.filter(s => !s.connectionId);

puppeteer.history()

列出最近的会话(包括打开和已关闭的会话)。

签名:

await puppeteer.history(binding: Fetcher): Promise<HistoryEntry[]>

返回值:

interface HistoryEntry {
  sessionId: string;
  startTime: number;
  endTime?: number;
  closeReason?: number;
  closeReasonText?: string; // "NormalClosure", "BrowserIdle", etc.
}

使用场景: 监控使用模式并调试会话问题。


puppeteer.limits()

检查当前账户限额和可用会话。

签名:

await puppeteer.limits(binding: Fetcher): Promise<LimitsInfo>

返回:

interface LimitsInfo {
  activeSessions: Array<{ id: string }>;
  maxConcurrentSessions: number;
  allowedBrowserAcquisitions: number;
  timeUntilNextAllowedBrowserAcquisition: number; // milliseconds
}

示例:

const limits = await puppeteer.limits(env.MYBROWSER);
if (limits.allowedBrowserAcquisitions === 0) {
  return new Response("Rate limit reached", { status: 429 });
}

Browser API

launch() 或 connect() 返回的 Browser 对象上可用的方法。

browser.newPage()

在浏览器中创建一个新页面(标签页)。

签名:

await browser.newPage(): Promise<Page>

示例:

const page = await browser.newPage();
await page.goto("https://example.com");

性能提示: 复用浏览器实例并打开多个标签页,而不是启动新浏览器。


browser.sessionId()

获取当前浏览器会话 ID。

返回: string - 会话 ID

示例:

const sessionId = browser.sessionId();
console.log("Current session:", sessionId);

browser.close()

关闭浏览器并终止会话。

签名:

await browser.close(): Promise<void>

适用场景: 完全结束浏览器使用并希望释放资源时。


browser.disconnect()

断开与浏览器的连接,但不关闭浏览器。

签名:

await browser.disconnect(): Promise<void>

适用场景: 复用会话——允许其他 Worker 稍后连接到同一会话。

示例:

// Keep session alive for reuse
const sessionId = browser.sessionId();
await browser.disconnect(); // Don't close, just disconnect
// Later: puppeteer.connect(env.MYBROWSER, sessionId)

browser.createBrowserContext()

创建一个隔离的无痕浏览器上下文。

签名:

await browser.createBrowserContext(): Promise<BrowserContext>

适用场景:

  • 在不同操作之间隔离 Cookie 和缓存
  • 测试多用户场景
  • 复用浏览器时保持会话隔离

示例:

const context1 = await browser.createBrowserContext();
const context2 = await browser.createBrowserContext();

const page1 = await context1.newPage();
const page2 = await context2.newPage();

// page1 and page2 have separate cookies/cache

Page API

browser.newPage() 返回的 Page 对象上可用的方法。

page.goto()

导航到一个 URL。

签名:

await page.goto(url: string, options?: NavigationOptions): Promise<Response>

选项:

  • waitUntil - 判定导航完成的时机:
    • "load" - 等待 load 事件(默认)
    • "domcontentloaded" - 等待 DOMContentLoaded
    • "networkidle0" - 等待 500 毫秒,直到没有网络连接
    • "networkidle2" - 等待 500 毫秒,直到网络连接数不超过 2
  • timeout - 最大导航时间,单位为毫秒(默认:30000)

示例:

await page.goto("https://example.com", {
  waitUntil: "networkidle0",
  timeout: 60000
});

最佳实践: 动态内容使用 "networkidle0",静态页面使用 "load"。


page.screenshot()

截取页面截图。

签名:

await page.screenshot(options?: ScreenshotOptions): Promise<Buffer>

选项:

  • fullPage (boolean) - 截取整个可滚动页面(默认:false)
  • type (string) - "png" 或 "jpeg"(默认:"png")
  • quality (number) - JPEG 质量,范围为 0-100(仅适用于 jpeg)
  • clip (object) - 截取指定区域:{ x, y, width, height }

示例:

// Full page screenshot
const screenshot = await page.screenshot({ fullPage: true });

// JPEG with compression
const screenshot = await page.screenshot({
  type: "jpeg",
  quality: 80
});

// Specific region
const screenshot = await page.screenshot({
  clip: { x: 0, y: 0, width: 800, height: 600 }
});

page.pdf()

生成页面的 PDF。

签名:

await page.pdf(options?: PDFOptions): Promise<Buffer>

选项:

  • format (string) - 页面格式:"Letter"、"A4" 等
  • printBackground (boolean) - 包含背景图形(默认:false)
  • margin (object) - { top, right, bottom, left }(例如:"1cm")
  • landscape (boolean) - 横向(默认:false)
  • scale (number) - 缩放比例,范围为 0.1-2(默认:1)

示例:

const pdf = await page.pdf({
  format: "A4",
  printBackground: true,
  margin: { top: "1cm", right: "1cm", bottom: "1cm", left: "1cm" }
});

return new Response(pdf, {
  headers: { "content-type": "application/pdf" }
});

page.content()

获取页面的完整 HTML 内容。

签名:

await page.content(): Promise<string>

示例:

const html = await page.content();
console.log(html); // Full HTML source

page.setContent()

设置自定义 HTML 内容。

签名:

await page.setContent(html: string, options?: NavigationOptions): Promise<void>

适用场景: 根据自定义 HTML 生成 PDF。

示例:

await page.setContent(`
  <!DOCTYPE html>
  <html>
    <head><style>body { font-family: Arial; }</style></head>
    <body><h1>Hello World</h1></body>
  </html>
`);

const pdf = await page.pdf({ format: "A4" });

page.evaluate()

在浏览器上下文中执行 JavaScript。

签名:

await page.evaluate<T>(fn: () => T): Promise<T>

适用场景:

  • 从 DOM 提取数据
  • 操作页面内容
  • 作为 XPath 的替代方案(不直接支持 XPath)

示例:

// Extract text content
const title = await page.evaluate(() => document.title);

// Extract structured data
const data = await page.evaluate(() => ({
  title: document.title,
  url: window.location.href,
  headings: Array.from(document.querySelectorAll("h1, h2")).map(el => el.textContent),
  links: Array.from(document.querySelectorAll("a")).map(el => el.href)
}));

// XPath workaround (XPath selectors not directly supported)
const innerHtml = await page.evaluate(() => {
  return new XPathEvaluator()
    .createExpression("/html/body/div/h1")
    .evaluate(document, XPathResult.FIRST_ORDERED_NODE_TYPE)
    .singleNodeValue.innerHTML;
});

page.waitForSelector()

等待元素出现在 DOM 中。

签名:

await page.waitForSelector(selector: string, options?: WaitForOptions): Promise<ElementHandle>

选项:

  • timeout (number) - 最大等待时间,单位为毫秒
  • visible (boolean) - 等待元素可见

示例:

await page.goto("https://example.com");
await page.waitForSelector("#content", { visible: true });
const screenshot = await page.screenshot();

page.type()

在输入框中输入文本。

签名:

await page.type(selector: string, text: string): Promise<void>

示例:

await page.type('input[name="email"]', '[email protected]');

page.click()

点击元素。

签名:

await page.click(selector: string): Promise<void>

示例:

await page.click('button[type="submit"]');
await page.waitForNavigation();

Playwright API 参考

Playwright 提供与 Puppeteer 类似的 API,但略有差异。

安装
npm install @cloudflare/playwright
基本示例
import { env } from "cloudflare:test";
import { chromium } from "@cloudflare/playwright";

interface Env {
  BROWSER: Fetcher;
}

export default {
  async fetch(request: Request, env: Env): Promise<Response> {
    const browser = await chromium.launch(env.BROWSER);
    const page = await browser.newPage();

    await page.goto("https://example.com");
    const screenshot = await page.screenshot();

    await browser.close();

    return new Response(screenshot, {
      headers: { "content-type": "image/png" }
    });
  }
};
与 Puppeteer 的主要区别
功能PuppeteerPlaywright
导入import puppeteer from "@cloudflare/puppeteer"import { chromium } from "@cloudflare/playwright"
启动puppeteer.launch(env.MYBROWSER)chromium.launch(env.BROWSER)
会话 API✅ 高级(会话、历史记录、限制)⚠️ 基础
自动等待手动调用 waitForSelector()内置自动等待
选择器仅 CSSCSS、文本、XPath(通过 evaluate 变通实现)

建议:除非你已有需要迁移的 Playwright 测试,否则请继续使用 Puppeteer。


会话管理

会话管理为何重要

问题:启动新浏览器速度慢,还会占用并发额度。

解决方案:在多个请求间复用浏览器会话。

优点:

  • ⚡ 速度更快(无需冷启动)
  • 💰 占用更少并发额度
  • 📊 更好地利用资源

会话复用模式
import puppeteer from "@cloudflare/puppeteer";

async function getBrowser(env: Env): Promise<{ browser: Browser; launched: boolean }> {
  // Check for available sessions
  const sessions = await puppeteer.sessions(env.MYBROWSER);
  const freeSessions = sessions.filter(s => !s.connectionId);

  if (freeSessions.length > 0) {
    // Reuse existing session
    try {
      const browser = await puppeteer.connect(env.MYBROWSER, freeSessions[0].sessionId);
      return { browser, launched: false };
    } catch (e) {
      console.log("Failed to connect, launching new browser");
    }
  }

  // Launch new session
  const browser = await puppeteer.launch(env.MYBROWSER);
  return { browser, launched: true };
}

export default {
  async fetch(request: Request, env: Env): Promise<Response> {
    const { browser, launched } = await getBrowser(env);

    try {
      const page = await browser.newPage();
      await page.goto("https://example.com");
      const screenshot = await page.screenshot();

      // Disconnect (don't close) to allow reuse
      await browser.disconnect();

      return new Response(screenshot, {
        headers: { "content-type": "image/png" }
      });
    } catch (error) {
      await browser.close(); // Close on error
      throw error;
    }
  }
};

重要:

  • 使用 browser.disconnect() 保持会话运行
  • 出错时使用 browser.close()
  • 始终处理连接失败

无痕浏览器上下文

使用浏览器上下文,在共享浏览器实例的同时隔离 Cookie 和缓存。

优点:

  • 共享浏览器(减少并发占用)
  • 隔离会话(分别使用 Cookie 和缓存)
  • 测试多用户场景

示例:

const browser = await puppeteer.launch(env.MYBROWSER);

// Create isolated contexts
const context1 = await browser.createBrowserContext();
const context2 = await browser.createBrowserContext();

// Each context has its own cookies/cache
const page1 = await context1.newPage();
const page2 = await context2.newPage();

await page1.goto("https://app.example.com"); // User 1
await page2.goto("https://app.example.com"); // User 2

await context1.close();
await context2.close();
await browser.close();

多个标签页与多个浏览器

场景:抓取 10 个 URL

❌ 不佳(10 个浏览器):

for (const url of urls) {
  const browser = await puppeteer.launch(env.MYBROWSER); // 10 launches!
  // ... scrape ...
  await browser.close();
}

✅ 更佳(1 个浏览器,10 个标签页):

const browser = await puppeteer.launch(env.MYBROWSER);

const results = await Promise.all(
  urls.map(async (url) => {
    const page = await browser.newPage();
    await page.goto(url);
    const data = await page.evaluate(() => ({
      title: document.title,
      text: document.body.innerText
    }));
    await page.close();
    return { url, data };
  })
);

await browser.close();

优点:只占用 1 个并发浏览器,而不是 10 个。


常见模式

模式 1:使用 KV 缓存截图

缓存截图,以减少浏览器使用量并提升性能。

import puppeteer from "@cloudflare/puppeteer";

interface Env {
  MYBROWSER: Fetcher;
  CACHE: KVNamespace;
}

export default {
  async fetch(request: Request, env: Env): Promise<Response> {
    const { searchParams } = new URL(request.url);
    const url = searchParams.get("url");

    if (!url) {
      return new Response("Missing ?url parameter", { status: 400 });
    }

    const normalizedUrl = new URL(url).toString();

    // Check cache
    let screenshot = await env.CACHE.get(normalizedUrl, { type: "arrayBuffer" });

    if (!screenshot) {
      // Generate screenshot
      const browser = await puppeteer.launch(env.MYBROWSER);
      const page = await browser.newPage();
      await page.goto(normalizedUrl);
      screenshot = await page.screenshot();
      await browser.close();

      // Cache for 24 hours
      await env.CACHE.put(normalizedUrl, screenshot, {
        expirationTtl: 60 * 60 * 24
      });
    }

    return new Response(screenshot, {
      headers: { "content-type": "image/png" }
    });
  }
};

模式 2:从 HTML 生成 PDF

将自定义 HTML 转换为 PDF。

import puppeteer from "@cloudflare/puppeteer";

interface Env {
  MYBROWSER: Fetcher;
}

export default {
  async fetch(request: Request, env: Env): Promise<Response> {
    if (request.method !== "POST") {
      return new Response("Method not allowed", { status: 405 });
    }

    const { html } = await request.json<{ html: string }>();

    const browser = await puppeteer.launch(env.MYBROWSER);
    const page = await browser.newPage();

    // Set custom HTML
    await page.setContent(html, { waitUntil: "networkidle0" });

    // Generate PDF
    const pdf = await page.pdf({
      format: "A4",
      printBackground: true,
      margin: {
        top: "1cm",
        right: "1cm",
        bottom: "1cm",
        left: "1cm"
      }
    });

    await browser.close();

    return new Response(pdf, {
      headers: {
        "content-type": "application/pdf",
        "content-disposition": "attachment; filename=document.pdf"
      }
    });
  }
};

模式 3:使用结构化数据进行网页抓取

从网页中提取结构化数据。

import puppeteer from "@cloudflare/puppeteer";

interface Env {
  MYBROWSER: Fetcher;
}

interface ProductData {
  title: string;
  price: string;
  description: string;
  image: string;
}

export default {
  async fetch(request: Request, env: Env): Promise<Response> {
    const { searchParams } = new URL(request.url);
    const url = searchParams.get("url");

    const browser = await puppeteer.launch(env.MYBROWSER);
    const page = await browser.newPage();

    await page.goto(url!, { waitUntil: "networkidle0" });

    // Extract structured data
    const data = await page.evaluate<ProductData>(() => {
      return {
        title: document.querySelector("h1")?.textContent || "",
        price: document.querySelector(".price")?.textContent || "",
        description: document.querySelector(".description")?.textContent || "",
        image: document.querySelector("img")?.src || ""
      };
    });

    await browser.close();

    return Response.json({ url, data });
  }
};

模式 4:批量抓取多个 URL

使用标签页高效抓取多个 URL。

import puppeteer from "@cloudflare/puppeteer";

interface Env {
  MYBROWSER: Fetcher;
}

async function scrapeUrl(browser: Browser, url: string): Promise<any> {
  const page = await browser.newPage();
  try {
    await page.goto(url, { waitUntil: "networkidle0", timeout: 30000 });

    const data = await page.evaluate(() => ({
      title: document.title,
      url: window.location.href,
      text: document.body.innerText.slice(0, 500) // First 500 chars
    }));

    await page.close();
    return { success: true, url, data };
  } catch (error) {
    await page.close();
    return {
      success: false,
      url,
      error: error instanceof Error ? error.message : "Unknown error"
    };
  }
}

export default {
  async fetch(request: Request, env: Env): Promise<Response> {
    const { urls } = await request.json<{ urls: string[] }>();

    if (!urls || urls.length === 0) {
      return new Response("Missing urls array", { status: 400 });
    }

    const browser = await puppeteer.launch(env.MYBROWSER);

    // Scrape all URLs in parallel (each in its own tab)
    const results = await Promise.all(
      urls.map(url => scrapeUrl(browser, url))
    );

    await browser.close();

    return Response.json({ results });
  }
};

模式 5:AI 增强型抓取

将 Browser Rendering 与 Workers AI 结合,以提取结构化数据。

import puppeteer from "@cloudflare/puppeteer";

interface Env {
  MYBROWSER: Fetcher;
  AI: Ai;
}

export default {
  async fetch(request: Request, env: Env): Promise<Response> {
    const { searchParams } = new URL(request.url);
    const url = searchParams.get("url");

    // Scrape page content
    const browser = await puppeteer.launch(env.MYBROWSER);
    const page = await browser.newPage();
    await page.goto(url!, { waitUntil: "networkidle0" });

    const bodyContent = await page.$eval("body", el => el.innerHTML);
    await browser.close();

    // Extract structured data with AI
    const response = await env.AI.run("@cf/meta/llama-3.1-8b-instruct", {
      messages: [
        {
          role: "user",
          content: `Extract product information as JSON from this HTML. Include: name, price, description, availability.\n\nHTML:\n${bodyContent.slice(0, 4000)}`
        }
      ]
    });

    // Parse AI response
    let productData;
    try {
      productData = JSON.parse(response.response);
    } catch {
      productData = { raw: response.response };
    }

    return Response.json({ url, product: productData });
  }
};

模式 6:表单填写与自动化

自动提交表单并执行多步骤工作流。

import puppeteer from "@cloudflare/puppeteer";

interface Env {
  MYBROWSER: Fetcher;
}

export default {
  async fetch(request: Request, env: Env): Promise<Response> {
    const { email, password } = await request.json<{
      email: string;
      password: string;
    }>();

    const browser = await puppeteer.launch(env.MYBROWSER);
    const page = await browser.newPage();

    // Navigate to login page
    await page.goto("https://example.com/login");

    // Fill form
    await page.type('input[name="email"]', email);
    await page.type('input[name="password"]', password);

    // Submit and wait for navigation
    await page.click('button[type="submit"]');
    await page.waitForNavigation();

    // Extract result
    const result = await page.evaluate(() => ({
      url: window.location.href,
      title: document.title,
      loggedIn: document.querySelector(".user-profile") !== null
    }));

    await browser.close();

    return Response.json(result);
  }
};

定价与限制

免费套餐(Workers Free)
功能限制
浏览器使用时长每天 10 分钟
并发浏览器数每个账户 3 个
每分钟新建浏览器数每分钟 3 个
REST API 请求数每分钟 6 次
浏览器超时60 秒(空闲时)
付费套餐(Workers Paid)
功能套餐内额度超出套餐额度
浏览器使用时长每月 10 小时每增加 1 个浏览器小时收费 $0.09
并发浏览器数10 个(月平均值)每增加 1 个并发浏览器收费 $2.00
每分钟新建浏览器数每分钟 30 个可申请提高限额
REST API 请求数每分钟 180 次可申请提高限额
浏览器超时60 秒(可通过 keep_alive 延长至 10 分钟)-
最大并发浏览器数每个账户 30 个可申请提高限额
定价计算

时长费用:

  • 按浏览器小时收费
  • 计费周期结束时四舍五入到最近的整小时
  • 失败的请求(超时)不收费

并发费用:

  • 按每日峰值用量的月平均值计算
  • 示例:15 天使用 10 个浏览器,15 天使用 20 个浏览器 = (10×15 + 20×15) / 30 = 平均 15 个
  • 平均 15 个 - 套餐内额度 10 个 = 5 × $2.00 = $10.00

月度账单示例:

  • 使用 50 个浏览器小时:(50 - 10) × $0.09 = $3.60
  • 平均并发 15 个浏览器:(15 - 10) × $2.00 = $10.00
  • 总计:$13.60
速率限制

每秒速率: 速率限制按秒执行。例如:

  • 每分钟 180 个请求 = 每秒 3 个请求
  • 不能一次性发送全部 180 个请求;必须均匀分散发送

处理速率限制:

async function launchWithRetry(env: Env, maxRetries = 3): Promise<Browser> {
  for (let i = 0; i < maxRetries; i++) {
    try {
      return await puppeteer.launch(env.MYBROWSER);
    } catch (error) {
      if (i === maxRetries - 1) throw error;

      // Check if rate limited
      const limits = await puppeteer.limits(env.MYBROWSER);
      if (limits.allowedBrowserAcquisitions === 0) {
        // Wait before retry
        const delay = limits.timeUntilNextAllowedBrowserAcquisition || 1000;
        await new Promise(resolve => setTimeout(resolve, delay));
      }
    }
  }
  throw new Error("Failed to launch browser");
}

已知问题预防

此技能可预防6 个已记录的问题:


问题 #1:不支持 XPath 选择器

错误:“不支持 XPath 选择器”或选择器失败
来源: https://developers.cloudflare.com/browser-rendering/faq/#why-cant-i-use-an-xpath-selector-when-using-browser-rendering-with-puppeteer
原因: XPath 会对 Workers 构成安全风险
**预防方法:**使用 CSS 选择器,或通过 XPathEvaluator 调用 page.evaluate()

解决方案:

// ❌ Don't use XPath directly (not supported)
// await page.$x('/html/body/div/h1')

// ✅ Use CSS selector
const heading = await page.$("div > h1");

// ✅ Or use XPath in page.evaluate()
const innerHtml = await page.evaluate(() => {
  return new XPathEvaluator()
    .createExpression("/html/body/div/h1")
    .evaluate(document, XPathResult.FIRST_ORDERED_NODE_TYPE)
    .singleNodeValue.innerHTML;
});

问题 #2:未传递浏览器绑定

错误:“无法读取 undefined 的属性(读取 'fetch')”
来源: https://developers.cloudflare.com/browser-rendering/faq/#cannot-read-properties-of-undefined-reading-fetch
**原因:**调用 puppeteer.launch() 时未传入浏览器绑定
**预防方法:**始终将 env.MYBROWSER 传递给 launch

解决方案:

// ❌ Missing browser binding
const browser = await puppeteer.launch(); // Error!

// ✅ Pass binding
const browser = await puppeteer.launch(env.MYBROWSER);

问题 #3:浏览器超时(60 秒)

**错误:**浏览器在 60 秒后意外关闭
来源: https://developers.cloudflare.com/browser-rendering/platform/limits/#note-on-browser-timeout
**原因:**默认超时时间为不活动 60 秒
**预防方法:**使用 keep_alive 选项将时间延长至最多 10 分钟

解决方案:

// Extend timeout to 5 minutes for long-running tasks
const browser = await puppeteer.launch(env.MYBROWSER, {
  keep_alive: 300000 // 5 minutes = 300,000 ms
});

**注意:**如果在指定时长内没有 devtools 命令,浏览器就会关闭。


问题 #4:达到并发限制

错误:“超出速率限制”或无法启动新浏览器
来源: https://developers.cloudflare.com/browser-rendering/platform/limits/
**原因:**超出并发浏览器数量限制(免费版为 3 个,付费版为 10–30 个)
**预防方法:**复用会话、使用标签页而非多个浏览器,并在启动前检查限制

解决方案:

// 1. Check limits before launching
const limits = await puppeteer.limits(env.MYBROWSER);
if (limits.allowedBrowserAcquisitions === 0) {
  return new Response("Concurrency limit reached", { status: 429 });
}

// 2. Reuse sessions
const sessions = await puppeteer.sessions(env.MYBROWSER);
const freeSessions = sessions.filter(s => !s.connectionId);
if (freeSessions.length > 0) {
  const browser = await puppeteer.connect(env.MYBROWSER, freeSessions[0].sessionId);
}

// 3. Use tabs instead of multiple browsers
const browser = await puppeteer.launch(env.MYBROWSER);
const page1 = await browser.newPage();
const page2 = await browser.newPage(); // Same browser, different tabs

问题 #5:本地开发请求大小限制

**错误:**在 wrangler dev 中,超过 1MB 的请求会失败
来源: https://developers.cloudflare.com/browser-rendering/faq/#does-local-development-support-all-browser-rendering-features
**原因:**本地开发限制
**预防方法:**在浏览器绑定中使用 remote: true 进行本地开发

解决方案:

// wrangler.jsonc for local development
{
  "browser": {
    "binding": "MYBROWSER",
    "remote": true  // Use real headless browser during dev
  }
}

问题 #6:始终触发机器人防护

**错误:**网站将请求拦截为机器人流量
来源: https://developers.cloudflare.com/browser-rendering/faq/#will-browser-rendering-bypass-cloudflares-bot-protection
**原因:**Browser Rendering 请求始终会被识别为机器人
**预防方法:**无法绕过;如果要抓取自己的区域,请创建 WAF 跳过规则

解决方案:

// ❌ Cannot bypass bot protection
// Requests will always be identified as bots

// ✅ If scraping your own Cloudflare zone:
// 1. Go to Security > WAF > Custom rules
// 2. Create skip rule with custom header:
//    Header: X-Custom-Auth
//    Value: your-secret-token
// 3. Pass header in your scraping requests

// Note: Automatic headers are included:
// - cf-biso-request-id
// - cf-biso-devtools

生产环境检查清单

将 Browser Rendering Workers 部署到生产环境之前:

配置
  • 已在 wrangler.jsonc 中配置浏览器绑定
  • 已启用 nodejs_compat 标志(Browser Rendering 必需)
  • 如果任务耗时超过 60 秒,已设置 Keep-alive 超时时间
  • 如有需要,已为本地开发启用远程绑定
错误处理
  • 已为速率限制实现重试逻辑
  • 已为 page.goto()处理超时
  • 已在 try-finally 块中清理浏览器
  • 已在启动浏览器前检查并发限制
  • 浏览器不可用时可优雅降级
性能
  • 已为高流量路由实现会话复用
  • 使用多个标签页而非多个浏览器
  • 使用无痕上下文隔离会话
  • 为重复的屏幕截图/PDF 使用 KV 缓存
  • 批量处理操作以最大限度地利用浏览器
监控
安全
  • 对 URL 进行输入验证(防止 SSRF)
  • 设置超时限制以防止滥用
  • 对公开端点实施速率限制
  • 对敏感的抓取端点进行身份验证
  • 如果抓取自己的区域,配置 WAF 规则
测试
  • 使用不同页面尺寸测试截图捕获
  • 使用自定义 HTML 测试 PDF 生成
  • 使用动态内容(networkidle0)测试抓取
  • 测试错误场景(无效 URL、超时)
  • 进行负载测试,检查并发限制

错误处理模板

适用于生产环境的完整错误处理:

import puppeteer from "@cloudflare/puppeteer";

interface Env {
  MYBROWSER: Fetcher;
}

async function withBrowser<T>(
  env: Env,
  fn: (browser: Browser) => Promise<T>
): Promise<T> {
  let browser: Browser | null = null;

  try {
    // Check limits
    const limits = await puppeteer.limits(env.MYBROWSER);
    if (limits.allowedBrowserAcquisitions === 0) {
      throw new Error("Rate limit reached. Retry after: " + limits.timeUntilNextAllowedBrowserAcquisition + "ms");
    }

    // Launch or connect
    const sessions = await puppeteer.sessions(env.MYBROWSER);
    const freeSessions = sessions.filter(s => !s.connectionId);

    if (freeSessions.length > 0) {
      try {
        browser = await puppeteer.connect(env.MYBROWSER, freeSessions[0].sessionId);
      } catch {
        browser = await puppeteer.launch(env.MYBROWSER);
      }
    } else {
      browser = await puppeteer.launch(env.MYBROWSER);
    }

    // Execute user function
    const result = await fn(browser);

    // Disconnect (keep session alive)
    await browser.disconnect();

    return result;
  } catch (error) {
    // Close on error
    if (browser) {
      await browser.close();
    }
    throw error;
  }
}

export default {
  async fetch(request: Request, env: Env): Promise<Response> {
    try {
      const screenshot = await withBrowser(env, async (browser) => {
        const page = await browser.newPage();
        await page.goto("https://example.com", {
          waitUntil: "networkidle0",
          timeout: 30000
        });
        return await page.screenshot();
      });

      return new Response(screenshot, {
        headers: { "content-type": "image/png" }
      });
    } catch (error) {
      console.error("Browser error:", error);
      return new Response(
        JSON.stringify({
          error: error instanceof Error ? error.message : "Unknown error"
        }),
        { status: 500, headers: { "content-type": "application/json" } }
      );
    }
  }
};

使用捆绑资源

模板(templates/)

常见模式的即用型代码模板:

  • basic-screenshot.ts - 最简截图示例
  • screenshot-with-kv-cache.ts - 使用 KV 缓存的截图
  • pdf-generation.ts - 从 HTML 或 URL 生成 PDF
  • web-scraper-basic.ts - 基本网页抓取模式
  • web-scraper-batch.ts - 批量抓取多个 URL
  • session-reuse.ts - 通过复用会话提升性能
  • ai-enhanced-scraper.ts - 使用 Workers AI 进行抓取
  • playwright-example.ts - Playwright 替代方案示例
  • wrangler-browser-config.jsonc - 浏览器绑定配置

用法:

# Copy template to your project
cp ~/.claude/skills/cloudflare-browser-rendering/templates/basic-screenshot.ts src/index.ts
参考资料(references/)

深入文档:

  • session-management.md - 完整的会话复用指南
  • pricing-and-limits.md - 详细的定价说明
  • common-errors.md - 所有已知问题及解决方案
  • puppeteer-vs-playwright.md - 功能对比和迁移

加载时机: 实现高级模式或调试特定问题时查阅。


依赖项

必需:

可选:

相关技能:

  • cloudflare-worker-base - 使用 Hono 配置 Worker
  • cloudflare-kv - 用于截图的 KV 缓存
  • cloudflare-r2 - 用于存储生成文件的 R2
  • cloudflare-workers-ai - AI 增强型抓取

官方文档


软件包版本(已于 2025-10-22 验证)

{
  "dependencies": {
    "@cloudflare/puppeteer": "^1.0.4"
  },
  "devDependencies": {
    "@cloudflare/workers-types": "^4.20251014.0",
    "wrangler": "^4.43.0"
  }
}

替代方案(Playwright):

{
  "dependencies": {
    "@cloudflare/playwright": "^1.0.0"
  }
}

故障排除

问题:“Cannot read properties of undefined (reading 'fetch')”

解决方案: 将浏览器绑定传递给 puppeteer.launch():

const browser = await puppeteer.launch(env.MYBROWSER); // Not just puppeteer.launch()
问题:XPath 选择器不起作用

解决方案: 使用 CSS 选择器,或通过 XPathEvaluator 调用 page.evaluate()(参见 Issue #1)

问题:浏览器在 60 秒后关闭

解决方案: 使用 keep_alive 延长超时时间:

const browser = await puppeteer.launch(env.MYBROWSER, { keep_alive: 300000 });
问题:达到速率限制

解决方案: 复用会话、使用标签页,并在启动前检查限制(参见 Issue #4)

问题:本地开发请求超过 1 MB 时失败

解决方案: 在 wrangler.jsonc 中启用远程绑定:

{ "browser": { "binding": "MYBROWSER", "remote": true } }
问题:网站将请求拦截为机器人流量

解决方案: 无法绕过。如果是自己的区域,请创建 WAF 跳过规则(参见 Issue #6)


有问题?遇到故障?

  1. 查看 references/common-errors.md,了解详细解决方案
  2. 查看 references/session-management.md,了解性能优化方法
  3. 确认已在 wrangler.jsonc 中配置浏览器绑定
  4. 查看官方文档:https://developers.cloudflare.com/browser-rendering/
  5. 确保已启用 nodejs_compat 兼容性标志

评论

支持 Markdown