cloudflare-browser-rendering by jezweb/claude-skills
npx skills add https://github.com/jezweb/claude-skills --skill cloudflare-browser-rendering用于构建浏览器自动化工作流程的生产就绪知识领域。
状态 : 生产就绪 ✅ 最后更新 : 2026-01-21 依赖项 : cloudflare-worker-base (用于 Worker 设置) 最新版本 : @cloudflare/puppeteer@1.0.4, @cloudflare/playwright@1.1.0, wrangler@4.59.3
近期更新 (2025) :
wrangler.jsonc:
{
"name": "browser-worker",
"main": "src/index.ts",
"compatibility_date": "2023-03-14",
"compatibility_flags": ["nodejs_compat"],
"browser": {
"binding": "MYBROWSER"
}
}
广告位招租
在这里展示您的产品或服务
触达数万 AI 开发者,精准高效
为什么需要 nodejs_compat? Browser Rendering 需要 Node.js API 和 polyfill。
npm install @cloudflare/puppeteer
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";
// 启动浏览器
const browser = await puppeteer.launch(env.MYBROWSER);
const page = await browser.newPage();
// 导航并捕获
await page.goto(url);
const screenshot = await page.screenshot();
// 清理
await browser.close();
return new Response(screenshot, {
headers: { "content-type": "image/png" }
});
}
};
npx wrangler deploy
测试地址: https://your-worker.workers.dev/?url=https://example.com
关键事项:
env.MYBROWSER 传递给 puppeteer.launch() (不能是 undefined)browser.close() (或使用 browser.disconnect() 以复用会话)nodejs_compat 兼容性标志Cloudflare Browser Rendering 提供在 Cloudflare 全球网络上运行的无头 Chromium 浏览器。使用熟悉的工具如 Puppeteer 和 Playwright 来自动化浏览器任务:
| 方法 | 最适合 | 复杂度 |
|---|---|---|
| Workers 绑定 | 复杂自动化、自定义工作流程、会话管理 | 高级 |
| REST API | 简单的截图/PDF 任务 | 简单 |
本技能涵盖 Workers 绑定 (具有完整 Puppeteer/Playwright API 的高级方法)。
| 特性 | Puppeteer | Playwright |
|---|---|---|
| API 熟悉度 | 最流行 | 采用率增长中 |
| 包 | @cloudflare/puppeteer@1.0.4 | @cloudflare/playwright@1.0.0 |
| 会话管理 | ✅ 高级 API | ⚠️ 基础 |
| 浏览器支持 | 仅 Chromium | 仅 Chromium (Firefox/Safari 尚未支持) |
| 最适合 | 截图、PDF、抓取 | 测试、前端自动化 |
推荐 : 对于大多数用例使用 Puppeteer。如果您已经在测试中使用 Playwright,则它是理想选择。
核心 API (完整参考: https://pptr.dev/api/):
全局函数:
puppeteer.launch(env.MYBROWSER, options?) - 启动新浏览器 (关键: 必须传递绑定)puppeteer.connect(env.MYBROWSER, sessionId) - 连接到现有会话puppeteer.sessions(env.MYBROWSER) - 列出运行中的会话puppeteer.history(env.MYBROWSER) - 列出最近会话 (打开 + 已关闭)puppeteer.limits(env.MYBROWSER) - 检查账户限制浏览器方法:
browser.newPage() - 创建新标签页 (优于启动新浏览器)browser.sessionId() - 获取会话 ID 以供复用browser.close() - 终止会话browser.disconnect() - 保持会话活跃以供复用browser.createBrowserContext() - 隔离的无痕上下文 (独立的 cookies/缓存)页面方法:
page.goto(url, { waitUntil, timeout }) - 导航 (对动态内容使用 "networkidle0")page.screenshot({ fullPage, type, quality, clip }) - 捕获图像page.pdf({ format, printBackground, margin }) - 生成 PDFpage.evaluate(() => ...) - 在浏览器中执行 JS (数据提取,XPath 变通方案)page.content() / page.setContent(html) - 获取/设置 HTMLpage.waitForSelector(selector) - 等待元素page.type(selector, text) / page.click(selector) - 表单交互关键模式:
// 必须传递绑定
const browser = await puppeteer.launch(env.MYBROWSER); // ✅
// const browser = await puppeteer.launch(); // ❌ 错误!
// 会话复用以提高性能
const sessions = await puppeteer.sessions(env.MYBROWSER);
const freeSessions = sessions.filter(s => !s.connectionId);
if (freeSessions.length > 0) {
browser = await puppeteer.connect(env.MYBROWSER, freeSessions[0].sessionId);
}
// 保持会话活跃
await browser.disconnect(); // 不要关闭
// XPath 变通方案 (不直接支持)
const data = await page.evaluate(() => {
return new XPathEvaluator()
.createExpression("/html/body/div/h1")
.evaluate(document, XPathResult.FIRST_ORDERED_NODE_TYPE)
.singleNodeValue.innerHTML;
});
状态 : 正式发布 (2025年9月) - 支持 Playwright v1.55、MCP v0.0.30、本地开发支持 (wrangler@4.26.0+)
安装:
npm install @cloudflare/playwright
配置要求 (2025 年更新):
{
"compatibility_flags": ["nodejs_compat"],
"compatibility_date": "2025-09-15" // Playwright v1.55 必需
}
基本用法:
import { chromium } from "@cloudflare/playwright";
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();
Puppeteer 与 Playwright 对比:
puppeteer 对比从 "@cloudflare/playwright" 导入 { chromium }waitForSelector()推荐 : 对于会话复用模式使用 Puppeteer。如果迁移现有测试或需要 MCP 集成,则使用 Playwright。
为什么 : 启动新浏览器速度慢且消耗并发限制。复用会话以获得更快的响应、更低的并发使用率、更好的资源利用率。
async function getBrowser(env: Env): Promise<Browser> {
const sessions = await puppeteer.sessions(env.MYBROWSER);
const freeSessions = sessions.filter(s => !s.connectionId);
if (freeSessions.length > 0) {
try {
return await puppeteer.connect(env.MYBROWSER, freeSessions[0].sessionId);
} catch (e) {
console.log("Failed to connect, launching new browser");
}
}
return await puppeteer.launch(env.MYBROWSER);
}
export default {
async fetch(request: Request, env: Env): Promise<Response> {
const browser = await getBrowser(env);
try {
const page = await browser.newPage();
await page.goto("https://example.com");
const screenshot = await page.screenshot();
await browser.disconnect(); // ✅ 保持活跃以供复用
return new Response(screenshot, {
headers: { "content-type": "image/png" }
});
} catch (error) {
await browser.close(); // ❌ 出错时关闭
throw error;
}
}
};
关键规则:
browser.disconnect() - 保持会话活跃以供复用browser.close() - 仅在出错或真正完成时使用使用 browser.createBrowserContext() 来共享浏览器但隔离 cookies/缓存:
const browser = await puppeteer.launch(env.MYBROWSER);
const context1 = await browser.createBrowserContext(); // 用户 1
const context2 = await browser.createBrowserContext(); // 用户 2
const page1 = await context1.newPage();
const page2 = await context2.newPage();
// 每个上下文独立的 cookies/缓存
❌ 错误做法 : 为 10 个 URL 启动 10 个浏览器 (浪费并发) ✅ 正确做法 : 1 个浏览器,通过 Promise.all() + browser.newPage() 创建 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 }));
await page.close();
return { url, data };
})
);
await browser.close();
缓存截图以减少浏览器使用并提高性能:
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();
// 首先检查缓存
let screenshot = await env.CACHE.get(normalizedUrl, { type: "arrayBuffer" });
if (!screenshot) {
const browser = await puppeteer.launch(env.MYBROWSER);
const page = await browser.newPage();
await page.goto(normalizedUrl);
screenshot = await page.screenshot();
await browser.close();
// 缓存 24 小时
await env.CACHE.put(normalizedUrl, screenshot, { expirationTtl: 60 * 60 * 24 });
}
return new Response(screenshot, { headers: { "content-type": "image/png" } });
}
};
结合 Browser Rendering 与 Workers AI 进行结构化数据提取:
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");
// 抓取页面内容
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();
// 使用 AI 提取结构化数据
const response = await env.AI.run("@cf/meta/llama-3.1-8b-instruct", {
messages: [{
role: "user",
content: `Extract product info as JSON from this HTML. Include: name, price, description.\n\nHTML:\n${bodyContent.slice(0, 4000)}`
}]
});
return Response.json({ url, product: JSON.parse(response.response) });
}
};
其他常用模式 : PDF 生成 (page.pdf())、结构化抓取 (page.evaluate())、表单自动化 (page.type() + page.click())。参见捆绑的 templates/ 目录。
计费正式发布 : 2025年8月20日
免费套餐 : 10 分钟/天,3 个并发,3 次启动/分钟,60 秒超时 付费套餐 : 包含 10 小时/月 (之后 $0.09/小时),30 个并发 (之后 $2.00/浏览器),30 次启动/分钟,60 秒-10 分钟超时
并发计算 : 每日峰值使用量的月平均值 (例如,35 个浏览器平均值 = (35 - 30 包含) × $2.00 = $10.00/月)
速率限制 : 使用固定的每秒填充速率强制执行 (不适用于突发)。30 次请求/分钟 = 每 2 秒 1 次请求。即使配额未使用,您也无法一次性发送所有 30 个请求。在启动前检查 puppeteer.limits(env.MYBROWSER):
const limits = await puppeteer.limits(env.MYBROWSER);
if (limits.allowedBrowserAcquisitions === 0) {
const delay = limits.timeUntilNextAllowedBrowserAcquisition || 1000;
await new Promise(resolve => setTimeout(resolve, delay));
}
本技能预防 8 个已记录的问题 :
错误: "XPath selector not supported" 或选择器失败 来源: https://developers.cloudflare.com/browser-rendering/faq/#why-cant-i-use-an-xpath-selector-when-using-browser-rendering-with-puppeteer 发生原因: XPath 对 Workers 构成安全风险 预防: 使用 CSS 选择器或在 page.evaluate() 中使用 XPathEvaluator
解决方案:
// ❌ 不要直接使用 XPath (不支持)
// await page.$x('/html/body/div/h1')
// ✅ 使用 CSS 选择器
const heading = await page.$("div > h1");
// ✅ 或在 page.evaluate() 中使用 XPath
const innerHtml = await page.evaluate(() => {
return new XPathEvaluator()
.createExpression("/html/body/div/h1")
.evaluate(document, XPathResult.FIRST_ORDERED_NODE_TYPE)
.singleNodeValue.innerHTML;
});
错误: "Cannot read properties of undefined (reading 'fetch')" 或 "RPC receiver does not implement the method 'launch'" 来源: GitHub Issue #10772, https://developers.cloudflare.com/browser-rendering/faq/#cannot-read-properties-of-undefined-reading-fetch 发生原因: 调用 puppeteer.launch() 时未传递浏览器绑定,或尝试直接调用 env.MYBROWSER.launch()。浏览器绑定是一个 Fetcher (REST API 包装器),而不是浏览器实例。 预防: 始终将 env.MYBROWSER 传递给 puppeteer.launch() 或 chromium.launch() 包装器
解决方案:
// ❌ 缺少浏览器绑定
const browser = await puppeteer.launch(); // 错误!
// ❌ 错误 - 尝试直接在 Fetcher 上调用 launch()
const browser = await env.MYBROWSER.launch(); // "RPC receiver does not implement the method 'launch'"
// ✅ 将绑定传递给 Puppeteer/Playwright 包装器
const browser = await puppeteer.launch(env.MYBROWSER);
// 或对于 Playwright:
const browser = await chromium.launch(env.MYBROWSER);
TypeScript 类型解释:
interface Env {
MYBROWSER: Fetcher; // 它是一个 Fetcher,不是 Browser!
}
错误: 浏览器在 60 秒后意外关闭 来源: https://developers.cloudflare.com/browser-rendering/platform/limits/#note-on-browser-timeout 发生原因: 默认超时时间为 60 秒无活动 预防: 使用 keep_alive 选项延长至最多 10 分钟
解决方案:
// 将超时延长至 5 分钟以处理长时间运行的任务
const browser = await puppeteer.launch(env.MYBROWSER, {
keep_alive: 300000 // 5 分钟 = 300,000 毫秒
});
注意: 如果在指定持续时间内没有 devtools 命令,浏览器将关闭。
错误: "Rate limit exceeded" 或新浏览器启动失败 来源: https://developers.cloudflare.com/browser-rendering/platform/limits/, Changelog 2025-09-25 发生原因: 超出并发浏览器限制 (截至 2025年9月,免费 3 个,付费 30 个) 预防: 复用会话,使用标签页而非多个浏览器,启动前检查限制,将请求限制在每秒速率内
解决方案:
// 1. 启动前检查限制
const limits = await puppeteer.limits(env.MYBROWSER);
if (limits.allowedBrowserAcquisitions === 0) {
return new Response("Concurrency limit reached", { status: 429 });
}
// 2. 复用会话
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. 使用标签页而非多个浏览器
const browser = await puppeteer.launch(env.MYBROWSER);
const page1 = await browser.newPage();
const page2 = await browser.newPage(); // 同一浏览器,不同标签页
错误: 大于 1MB 的请求在 wrangler dev 中失败 来源: https://developers.cloudflare.com/browser-rendering/faq/#does-local-development-support-all-browser-rendering-features 发生原因: 本地开发限制 预防: 在本地开发中使用浏览器绑定的 remote: true
解决方案:
// 用于本地开发的 wrangler.jsonc
{
"browser": {
"binding": "MYBROWSER",
"remote": true // 在开发期间使用真实的无头浏览器
}
}
错误: 网站将请求阻止为机器人流量 来源: https://developers.cloudflare.com/browser-rendering/faq/#will-browser-rendering-bypass-cloudflares-bot-protection 发生原因: Browser Rendering 请求始终被识别为机器人 预防: 无法绕过;如果抓取您自己的区域,请创建 WAF 跳过规则 (需要企业计划以使用 Bot Management)
解决方案:
// ❌ 无法绕过机器人防护
// 请求将始终被识别为机器人
// ✅ 如果抓取您自己的 Cloudflare 区域 (仅限企业计划):
// 1. 转到 Security > WAF > Custom rules
// 2. 使用自定义请求头创建跳过规则:
// 请求头: X-Custom-Auth
// 值: your-secret-token
// 3. 在抓取请求中传递该请求头
await page.setExtraHTTPHeaders({
'X-Custom-Auth': 'your-secret-token'
});
// 注意: 自动包含以下请求头:
// - cf-biso-request-id
// - cf-biso-devtools
重要: 免费/专业/商业计划即使在自己的站点上也无法绕过机器人检测。需要具有 Bot Management 的企业计划才能进行 WAF 允许列表设置。
错误: ReferenceError: __name is not defined 来源: GitHub Issue #7107 发生原因: esbuild 压缩 (wrangler 3.80.1+) 在包含嵌套函数声明的箭头函数中注入 __name() 辅助调用。这些在浏览器上下文中运行,而辅助函数不存在。 预防: 保持 page.evaluate() 函数简单 - 避免嵌套函数声明 适用于: wrangler 3.80.1 - 3.83.0 (在 3.83.0+ 中修复)
解决方案:
// ❌ 避免嵌套函数声明
const data = await page.evaluate(async () => {
function toNumber(str: string | undefined): number | undefined {
const num = typeof str === 'string' ? str.replaceAll('.', '').replaceAll(',', '.').match(/[+-]?([0-9]*[.])?[0-9]+/) : false;
if (num) {
return Number(num[0]);
} else {
return undefined;
}
}
return toNumber('123.456');
});
// 错误: ReferenceError: __name is not defined
// ✅ 内联逻辑而不使用嵌套函数
const data = await page.evaluate(async () => {
const str = '123.456';
const num = typeof str === 'string' ? str.replaceAll('.', '').replaceAll(',', '.').match(/[+-]?([0-9]*[.])?[0-9]+/) : false;
return num ? Number(num[0]) : undefined;
});
// ✅ 或更新到 wrangler 3.83.0+
// npm install wrangler@latest
注意: 这也影响具有复杂回调的 page.waitForSelector()。已在 wrangler 3.83.0+ (2024年11月) 中修复。
错误: 依赖无限等待的代码现在会超时 来源: Changelog 2026-01-07 发生原因: waitForSelector() 以前在未找到选择器时不会超时 (无限挂起)。现已修复以正确遵守超时值。 预防: 始终设置显式超时并处理超时错误 适用于: 所有在 2026年1月之前编写并依赖无限等待的代码
解决方案:
// ❌ 旧行为 - 如果未找到选择器将永远挂起
await page.waitForSelector('#dynamic-element');
// ✅ 新行为 - 正确超时 (设置显式超时)
try {
await page.waitForSelector('#dynamic-element', { timeout: 5000 });
} catch (error) {
if (error.name === 'TimeoutError') {
console.log('Element not found within 5 seconds');
// 优雅处理缺失元素
} else {
throw error;
}
}
// ✅ 对加载缓慢的元素使用更长的超时
await page.waitForSelector('#slow-element', { timeout: 30000 }); // 30 秒
注意: 这是一个破坏性修复 (行为变更)。依赖无限等待的代码现在将超时并抛出错误。始终优雅地处理 TimeoutError。
将 Browser Rendering Workers 部署到生产环境前:
生产模式 - 使用 try-catch 进行适当清理:
async function withBrowser<T>(env: Env, fn: (browser: Browser) => Promise<T>): Promise<T> {
let browser: Browser | null = null;
try {
// 1. 启动前检查限制
const limits = await puppeteer.limits(env.MYBROWSER);
if (limits.allowedBrowserAcquisitions === 0) {
throw new Error("Rate limit reached");
}
// 2. 首先尝试会话复用
const sessions = await puppeteer.sessions(env.MYBROWSER);
const freeSessions = sessions.filter(s => !s.connectionId);
browser = freeSessions.length > 0
? await puppeteer.connect(env.MYBROWSER, freeSessions[0].sessionId)
: await puppeteer.launch(env.MYBROWSER);
// 3. 执行用户函数
const result = await fn(browser);
// 4. 断开连接 (保持活跃)
await browser.disconnect();
return result;
} catch (error) {
// 5. 出错时关闭
if (browser) await browser.close();
throw error;
}
}
关键原则 : 检查限制 → 复用会话 → 执行 → 成功时断开连接,出错时关闭
用于常见模式的即用型代码模板:
basic-screenshot.ts - 最小截图示例screenshot-with-kv-cache.ts - 带 KV 缓存的截图pdf-generation.ts - 从 HTML 或 URL 生成 PDFweb-scraper-basic.ts - 基本网页抓取模式web-scraper-batch.ts - 批量抓取多个 URLsession-reuse.ts - 会话复用以提高性能ai-enhanced-scraper.ts - 使用 Workers AI 的抓取playwright-example.ts - Playwright 替代示例wrangler-browser-config.jsonc - 浏览器绑定配置用法:
# 复制模板到您的项目
cp ~/.claude/skills/cloudflare-browser-rendering/templates/basic-screenshot.ts src/index.ts
深入文档:
session-management.md - 完整的会话复用指南pricing-and-limits.md - 详细的定价明细common-errors.md - 所有已知问题及解决方案puppeteer-vs-playwright.md - 功能对比和迁移何时加载: 在实现高级模式或调试特定问题时参考。
必需:
@cloudflare/puppeteer@1.0.4 - 用于 Workers 的 Puppeteerwrangler@4.43.0+ - Cloudflare CLI可选:
@cloudflare/playwright@1.0.0 - 用于 Workers 的 Playwright (替代方案)@cloudflare/workers-types@4.20251014.0+ - TypeScript 类型相关技能:
cloudflare-worker-base - 使用 Hono 的 Worker 设置cloudflare-kv - 用于截图的 KV 缓存cloudflare-r2 - 用于生成文件的 R2 存储cloudflare-workers-ai - AI 增强的抓取{
"dependencies": {
"@cloudflare/puppeteer": "^1.0.4"
},
"devDependencies": {
"@cloudflare/workers-types": "^4.20251014.0",
"wrangler": "^4.59.3"
}
}
替代方案 (Playwright):
{
"dependencies": {
"@cloudflare/playwright": "^1.1.0"
}
}
注意: Playwright v1.1.0 包含对 Playwright v1.57 (2026年1月) 的支持。Wrangler 3.83.0+ 修复了 page.evaluate() __name 注入错误。
解决方案: 将浏览器绑定传递给 puppeteer.launch():
const browser = await puppeteer.launch(env.MYBROWSER); // 不仅仅是 puppeteer.launch()
解决方案: 使用 CSS 选择器或在 page.evaluate() 中使用 XPathEvaluator (参见问题 #1)
解决方案: 使用 keep_alive 延长超时:
const browser = await puppeteer.launch(env.MYBROWSER, { keep_alive: 300000 });
解决方案: 复用会话,使用标签页,启动前检查限制 (参见问题 #4)
解决方案: 在 wrangler.jsonc 中启用远程绑定:
{ "browser": { "binding": "MYBROWSER", "remote": true } }
解决方案: 无法绕过。如果是您自己的区域,创建 WAF 跳过规则 (参见问题 #6)
**
Production-ready knowledge domain for building browser automation workflows with Cloudflare Browser Rendering.
Status : Production Ready ✅ Last Updated : 2026-01-21 Dependencies : cloudflare-worker-base (for Worker setup) Latest Versions : @cloudflare/puppeteer@1.0.4, @cloudflare/playwright@1.1.0, wrangler@4.59.3
Recent Updates (2025) :
wrangler.jsonc:
{
"name": "browser-worker",
"main": "src/index.ts",
"compatibility_date": "2023-03-14",
"compatibility_flags": ["nodejs_compat"],
"browser": {
"binding": "MYBROWSER"
}
}
Why nodejs_compat? Browser Rendering requires Node.js APIs and polyfills.
npm install @cloudflare/puppeteer
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" }
});
}
};
npx wrangler deploy
Test at: https://your-worker.workers.dev/?url=https://example.com
CRITICAL:
env.MYBROWSER to puppeteer.launch() (not undefined)browser.close() when done (or use browser.disconnect() for session reuse)nodejs_compat compatibility flagCloudflare Browser Rendering provides headless Chromium browsers running on Cloudflare's global network. Use familiar tools like Puppeteer and Playwright to automate browser tasks:
| Method | Best For | Complexity |
|---|---|---|
| Workers Bindings | Complex automation, custom workflows, session management | Advanced |
| REST API | Simple screenshot/PDF tasks | Simple |
This skill covers Workers Bindings (the advanced method with full Puppeteer/Playwright APIs).
| Feature | Puppeteer | Playwright |
|---|---|---|
| API Familiarity | Most popular | Growing adoption |
| Package | @cloudflare/puppeteer@1.0.4 | @cloudflare/playwright@1.0.0 |
| Session Management | ✅ Advanced APIs | ⚠️ Basic |
| Browser Support | Chromium only | Chromium only (Firefox/Safari not yet supported) |
| Best For | Screenshots, PDFs, scraping | Testing, frontend automation |
Recommendation : Use Puppeteer for most use cases. Playwright is ideal if you're already using it for testing.
Core APIs (complete reference: https://pptr.dev/api/):
Global Functions:
puppeteer.launch(env.MYBROWSER, options?) - Launch new browser (CRITICAL: must pass binding)puppeteer.connect(env.MYBROWSER, sessionId) - Connect to existing sessionpuppeteer.sessions(env.MYBROWSER) - List running sessionspuppeteer.history(env.MYBROWSER) - List recent sessions (open + closed)puppeteer.limits(env.MYBROWSER) - Check account limitsBrowser Methods:
browser.newPage() - Create new tab (preferred over launching new browsers)browser.sessionId() - Get session ID for reusebrowser.close() - Terminate sessionbrowser.disconnect() - Keep session alive for reusebrowser.createBrowserContext() - Isolated incognito context (separate cookies/cache)Page Methods:
page.goto(url, { waitUntil, timeout }) - Navigate (use "networkidle0" for dynamic content)page.screenshot({ fullPage, type, quality, clip }) - Capture imagepage.pdf({ format, printBackground, margin }) - Generate PDFpage.evaluate(() => ...) - Execute JS in browser (data extraction, XPath workaround)page.content() / page.setContent(html) - Get/set HTMLpage.waitForSelector(selector) - Wait for elementpage.type(selector, text) / - Form interactionCritical Patterns:
// Must pass binding
const browser = await puppeteer.launch(env.MYBROWSER); // ✅
// const browser = await puppeteer.launch(); // ❌ Error!
// Session reuse for performance
const sessions = await puppeteer.sessions(env.MYBROWSER);
const freeSessions = sessions.filter(s => !s.connectionId);
if (freeSessions.length > 0) {
browser = await puppeteer.connect(env.MYBROWSER, freeSessions[0].sessionId);
}
// Keep session alive
await browser.disconnect(); // Don't close
// XPath workaround (not directly supported)
const data = await page.evaluate(() => {
return new XPathEvaluator()
.createExpression("/html/body/div/h1")
.evaluate(document, XPathResult.FIRST_ORDERED_NODE_TYPE)
.singleNodeValue.innerHTML;
});
Status : GA (Sept 2025) - Playwright v1.55, MCP v0.0.30 support, local dev support (wrangler@4.26.0+)
Installation:
npm install @cloudflare/playwright
Configuration Requirements (2025 Update):
{
"compatibility_flags": ["nodejs_compat"],
"compatibility_date": "2025-09-15" // Required for Playwright v1.55
}
Basic Usage:
import { chromium } from "@cloudflare/playwright";
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();
Puppeteer vs Playwright:
puppeteer vs { chromium } from "@cloudflare/playwright"waitForSelector()Recommendation : Use Puppeteer for session reuse patterns. Use Playwright if migrating existing tests or need MCP integration.
Official Docs : https://developers.cloudflare.com/browser-rendering/playwright/
Why : Launching new browsers is slow and consumes concurrency limits. Reuse sessions for faster response, lower concurrency usage, better resource utilization.
async function getBrowser(env: Env): Promise<Browser> {
const sessions = await puppeteer.sessions(env.MYBROWSER);
const freeSessions = sessions.filter(s => !s.connectionId);
if (freeSessions.length > 0) {
try {
return await puppeteer.connect(env.MYBROWSER, freeSessions[0].sessionId);
} catch (e) {
console.log("Failed to connect, launching new browser");
}
}
return await puppeteer.launch(env.MYBROWSER);
}
export default {
async fetch(request: Request, env: Env): Promise<Response> {
const browser = await getBrowser(env);
try {
const page = await browser.newPage();
await page.goto("https://example.com");
const screenshot = await page.screenshot();
await browser.disconnect(); // ✅ Keep alive for reuse
return new Response(screenshot, {
headers: { "content-type": "image/png" }
});
} catch (error) {
await browser.close(); // ❌ Close on error
throw error;
}
}
};
Key Rules:
browser.disconnect() - Keep session alive for reusebrowser.close() - Only on errors or when truly doneUse browser.createBrowserContext() to share browser but isolate cookies/cache:
const browser = await puppeteer.launch(env.MYBROWSER);
const context1 = await browser.createBrowserContext(); // User 1
const context2 = await browser.createBrowserContext(); // User 2
const page1 = await context1.newPage();
const page2 = await context2.newPage();
// Separate cookies/cache per context
❌ Bad : Launch 10 browsers for 10 URLs (wastes concurrency) ✅ Good : 1 browser, 10 tabs via Promise.all() + browser.newPage()
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 }));
await page.close();
return { url, data };
})
);
await browser.close();
Cache screenshots to reduce browser usage and improve performance:
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 first
let screenshot = await env.CACHE.get(normalizedUrl, { type: "arrayBuffer" });
if (!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" } });
}
};
Combine Browser Rendering with Workers AI for structured data extraction:
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 info as JSON from this HTML. Include: name, price, description.\n\nHTML:\n${bodyContent.slice(0, 4000)}`
}]
});
return Response.json({ url, product: JSON.parse(response.response) });
}
};
Other Common Patterns : PDF generation (page.pdf()), structured scraping (page.evaluate()), form automation (page.type() + page.click()). See bundled templates/ directory.
Billing GA : August 20, 2025
Free Tier : 10 min/day, 3 concurrent, 3 launches/min, 60s timeout Paid Tier : 10 hrs/month included ($0.09/hr after), 30 concurrent ($2.00/browser after), 30 launches/min, 60s-10min timeout
Concurrency Calculation : Monthly average of daily peak usage (e.g., 35 browsers avg = (35 - 30 included) × $2.00 = $10.00/mo)
Rate Limiting : Enforced with a fixed per-second fill rate (NOT burst-friendly). 30 req/min = 1 req every 2 seconds. You CANNOT send all 30 requests at once, even if quota is unused. Check puppeteer.limits(env.MYBROWSER) before launching:
const limits = await puppeteer.limits(env.MYBROWSER);
if (limits.allowedBrowserAcquisitions === 0) {
const delay = limits.timeUntilNextAllowedBrowserAcquisition || 1000;
await new Promise(resolve => setTimeout(resolve, delay));
}
This skill prevents 8 documented issues :
Error: "XPath selector not supported" or selector failures Source: https://developers.cloudflare.com/browser-rendering/faq/#why-cant-i-use-an-xpath-selector-when-using-browser-rendering-with-puppeteer Why It Happens: XPath poses a security risk to Workers Prevention: Use CSS selectors or page.evaluate() with XPathEvaluator
Solution:
// ❌ 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;
});
Error: "Cannot read properties of undefined (reading 'fetch')" or "RPC receiver does not implement the method 'launch'" Source: GitHub Issue #10772, https://developers.cloudflare.com/browser-rendering/faq/#cannot-read-properties-of-undefined-reading-fetch Why It Happens: puppeteer.launch() called without browser binding, or trying to call env.MYBROWSER.launch() directly. The browser binding is a Fetcher (REST API wrapper), not a browser instance. Prevention: Always pass env.MYBROWSER to puppeteer.launch() or chromium.launch() wrapper
Solution:
// ❌ Missing browser binding
const browser = await puppeteer.launch(); // Error!
// ❌ Wrong - trying to call launch() on Fetcher directly
const browser = await env.MYBROWSER.launch(); // "RPC receiver does not implement the method 'launch'"
// ✅ Pass binding to Puppeteer/Playwright wrapper
const browser = await puppeteer.launch(env.MYBROWSER);
// or for Playwright:
const browser = await chromium.launch(env.MYBROWSER);
TypeScript Type Explanation:
interface Env {
MYBROWSER: Fetcher; // It's a Fetcher, not a Browser!
}
Error: Browser closes unexpectedly after 60 seconds Source: https://developers.cloudflare.com/browser-rendering/platform/limits/#note-on-browser-timeout Why It Happens: Default timeout is 60 seconds of inactivity Prevention: Use keep_alive option to extend up to 10 minutes
Solution:
// Extend timeout to 5 minutes for long-running tasks
const browser = await puppeteer.launch(env.MYBROWSER, {
keep_alive: 300000 // 5 minutes = 300,000 ms
});
Note: Browser closes if no devtools commands for the specified duration.
Error: "Rate limit exceeded" or new browser launch fails Source: https://developers.cloudflare.com/browser-rendering/platform/limits/, Changelog 2025-09-25 Why It Happens: Exceeded concurrent browser limit (3 free, 30 paid as of Sept 2025) Prevention: Reuse sessions, use tabs instead of multiple browsers, check limits before launching, throttle requests to per-second rate
Solutions:
// 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
Error: Request larger than 1MB fails in wrangler dev Source: https://developers.cloudflare.com/browser-rendering/faq/#does-local-development-support-all-browser-rendering-features Why It Happens: Local development limitation Prevention: Use remote: true in browser binding for local dev
Solution:
// wrangler.jsonc for local development
{
"browser": {
"binding": "MYBROWSER",
"remote": true // Use real headless browser during dev
}
}
Error: Website blocks requests as bot traffic Source: https://developers.cloudflare.com/browser-rendering/faq/#will-browser-rendering-bypass-cloudflares-bot-protection Why It Happens: Browser Rendering requests always identified as bots Prevention: Cannot bypass; if scraping your own zone, create WAF skip rule (requires Enterprise plan for Bot Management)
Solution:
// ❌ Cannot bypass bot protection
// Requests will always be identified as bots
// ✅ If scraping your own Cloudflare zone (Enterprise plan only):
// 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
await page.setExtraHTTPHeaders({
'X-Custom-Auth': 'your-secret-token'
});
// Note: Automatic headers are included:
// - cf-biso-request-id
// - cf-biso-devtools
Important: Free/Pro/Business plans CANNOT bypass bot detection even on their own sites. Enterprise plan with Bot Management is required for WAF allowlisting.
Error: ReferenceError: __name is not defined Source: GitHub Issue #7107 Why It Happens: esbuild minification (wrangler 3.80.1+) injects __name() helper calls in arrow functions with nested function declarations. These run in browser context where the helper doesn't exist. Prevention: Keep page.evaluate() functions simple - avoid nested function declarations Applies to: wrangler 3.80.1 - 3.83.0 (fixed in 3.83.0+)
Solution:
// ❌ Avoid nested function declarations
const data = await page.evaluate(async () => {
function toNumber(str: string | undefined): number | undefined {
const num = typeof str === 'string' ? str.replaceAll('.', '').replaceAll(',', '.').match(/[+-]?([0-9]*[.])?[0-9]+/) : false;
if (num) {
return Number(num[0]);
} else {
return undefined;
}
}
return toNumber('123.456');
});
// Error: ReferenceError: __name is not defined
// ✅ Inline the logic without nested functions
const data = await page.evaluate(async () => {
const str = '123.456';
const num = typeof str === 'string' ? str.replaceAll('.', '').replaceAll(',', '.').match(/[+-]?([0-9]*[.])?[0-9]+/) : false;
return num ? Number(num[0]) : undefined;
});
// ✅ Or update to wrangler 3.83.0+
// npm install wrangler@latest
Note: This also affects page.waitForSelector() with complex callbacks. Fixed in wrangler 3.83.0+ (Nov 2024).
Error: Code that relied on indefinite waiting now times out Source: Changelog 2026-01-07 Why It Happens: waitForSelector() previously did NOT timeout when selectors weren't found (hung indefinitely). This was fixed to properly honor timeout values. Prevention: Always set explicit timeouts and handle timeout errors Applies to: All code written before Jan 2026 that relied on indefinite waiting
Solution:
// ❌ Old behavior - would hang forever if selector not found
await page.waitForSelector('#dynamic-element');
// ✅ New behavior - properly times out (set explicit timeout)
try {
await page.waitForSelector('#dynamic-element', { timeout: 5000 });
} catch (error) {
if (error.name === 'TimeoutError') {
console.log('Element not found within 5 seconds');
// Handle missing element gracefully
} else {
throw error;
}
}
// ✅ Use longer timeout for slow-loading elements
await page.waitForSelector('#slow-element', { timeout: 30000 }); // 30 seconds
Note: This is a breaking fix (behavior change). Code that relied on indefinite waiting will now timeout and throw errors. Always handle TimeoutError gracefully.
Before deploying Browser Rendering Workers to production:
Production Pattern - Use try-catch with proper cleanup:
async function withBrowser<T>(env: Env, fn: (browser: Browser) => Promise<T>): Promise<T> {
let browser: Browser | null = null;
try {
// 1. Check limits before launching
const limits = await puppeteer.limits(env.MYBROWSER);
if (limits.allowedBrowserAcquisitions === 0) {
throw new Error("Rate limit reached");
}
// 2. Try session reuse first
const sessions = await puppeteer.sessions(env.MYBROWSER);
const freeSessions = sessions.filter(s => !s.connectionId);
browser = freeSessions.length > 0
? await puppeteer.connect(env.MYBROWSER, freeSessions[0].sessionId)
: await puppeteer.launch(env.MYBROWSER);
// 3. Execute user function
const result = await fn(browser);
// 4. Disconnect (keep alive)
await browser.disconnect();
return result;
} catch (error) {
// 5. Close on error
if (browser) await browser.close();
throw error;
}
}
Key Principles : Check limits → Reuse sessions → Execute → Disconnect on success, close on error
Ready-to-use code templates for common patterns:
basic-screenshot.ts - Minimal screenshot examplescreenshot-with-kv-cache.ts - Screenshot with KV cachingpdf-generation.ts - Generate PDFs from HTML or URLsweb-scraper-basic.ts - Basic web scraping patternweb-scraper-batch.ts - Batch scrape multiple URLssession-reuse.ts - Session reuse for performanceai-enhanced-scraper.ts - Scraping with Workers AIplaywright-example.ts - Playwright alternative examplewrangler-browser-config.jsonc - Browser binding configurationUsage:
# Copy template to your project
cp ~/.claude/skills/cloudflare-browser-rendering/templates/basic-screenshot.ts src/index.ts
Deep-dive documentation:
session-management.md - Complete session reuse guidepricing-and-limits.md - Detailed pricing breakdowncommon-errors.md - All known issues and solutionspuppeteer-vs-playwright.md - Feature comparison and migrationWhen to load: Reference when implementing advanced patterns or debugging specific issues.
Required:
@cloudflare/puppeteer@1.0.4 - Puppeteer for Workerswrangler@4.43.0+ - Cloudflare CLIOptional:
@cloudflare/playwright@1.0.0 - Playwright for Workers (alternative)@cloudflare/workers-types@4.20251014.0+ - TypeScript typesRelated Skills:
cloudflare-worker-base - Worker setup with Honocloudflare-kv - KV caching for screenshotscloudflare-r2 - R2 storage for generated filescloudflare-workers-ai - AI-enhanced scraping{
"dependencies": {
"@cloudflare/puppeteer": "^1.0.4"
},
"devDependencies": {
"@cloudflare/workers-types": "^4.20251014.0",
"wrangler": "^4.59.3"
}
}
Alternative (Playwright):
{
"dependencies": {
"@cloudflare/playwright": "^1.1.0"
}
}
Note: Playwright v1.1.0 includes support for Playwright v1.57 (Jan 2026). Wrangler 3.83.0+ fixes the page.evaluate() __name injection bug.
Solution: Pass browser binding to puppeteer.launch():
const browser = await puppeteer.launch(env.MYBROWSER); // Not just puppeteer.launch()
Solution: Use CSS selectors or page.evaluate() with XPathEvaluator (see Issue #1)
Solution: Extend timeout with keep_alive:
const browser = await puppeteer.launch(env.MYBROWSER, { keep_alive: 300000 });
Solution: Reuse sessions, use tabs, check limits before launching (see Issue #4)
Solution: Enable remote binding in wrangler.jsonc:
{ "browser": { "binding": "MYBROWSER", "remote": true } }
Solution: Cannot bypass. If your own zone, create WAF skip rule (see Issue #6)
Questions? Issues?
references/common-errors.md for detailed solutionsreferences/session-management.md for performance optimizationnodejs_compat compatibility flag is enabledWeekly Installs
341
Repository
GitHub Stars
643
First Seen
Jan 20, 2026
Security Audits
Gen Agent Trust HubFailSocketPassSnykWarn
Installed on
claude-code271
gemini-cli226
opencode223
cursor209
antigravity203
codex196
Azure 升级评估与自动化工具 - 轻松迁移 Functions 计划、托管层级和 SKU
68,100 周安装
fp-check误报检查工具:验证漏洞真伪,提升安全分析准确性
725 周安装
Python图像处理技能:Pillow脚本生成,支持调整大小、格式转换、网页优化
721 周安装
Android设备自动化工具Midscene使用指南 - 基于AI的移动端自动化测试与操作
713 周安装
Sentry问题修复技能:利用AI自动调试与修复生产环境错误
732 周安装
App Store Connect CLI 崩溃排查工具:快速分析 TestFlight 崩溃报告与性能诊断
732 周安装
Uniwind:将Tailwind CSS v4引入React Native的样式解决方案
735 周安装
page.click(selector)