预渲染在将页面返回给客户端之前生成页面的最终 HTML。对于 JavaScript 密集型应用,这意味着在浏览器中加载页面,等待客户端 JavaScript 运行,然后返回渲染后的 HTML,而非初始应用 shell。
当搜索爬虫、社交预览 bot、AI 索引任务或合作伙伴集成需要你的应用通常在浏览器中创建的 HTML 内容时,预渲染很有用。通过 Cloudflare Browser Run 和 Cloudflare Workers,你可以在托管的无头 Chrome 中渲染公共 URL 并返回渲染后的 HTML。
在本教程中,你将:
- 向 Worker 添加 Browser Run 绑定
- 创建最小预渲染端点
- 限制 Worker 可以渲染的主机名
- 使用 remote 模式在本地测试端点
要跟随本教程,你需要:
- Cloudflare 账户
- 使用 TypeScript 的 Worker 项目
- 要预渲染的公共 URL
你预渲染的页面可以在任何地方运行。本教程中的 Worker 仅作为调用 Browser Run 的预渲染服务。
向 Wrangler 配置添加 Browser Run 绑定:
{
"$schema": "./node_modules/wrangler/config-schema.json",
"name": "my-prerender-worker",
"main": "src/index.ts",
// Set this to today's date
"compatibility_date": "2026-08-17",
"browser": {
"binding": "BROWSER"
}
}name = "my-prerender-worker"
main = "src/index.ts"
# Set this to today's date
compatibility_date = "2026-08-17"
[browser]
binding = "BROWSER"将 src/index.ts 的内容替换为以下 Worker。更新 ALLOWED_HOSTNAMES 以包含 Worker 可以预渲染的主机名。
// Only render pages you control. This prevents the Worker from becoming
// an open browser-rendering proxy for arbitrary websites.
const ALLOWED_HOSTNAMES = new Set(["example.com", "www.example.com"]);
const getTargetUrl = (request) => {
const requestUrl = new URL(request.url);
const target = requestUrl.searchParams.get("url");
if (!target) {
throw new Error("Missing url query parameter");
}
const targetUrl = new URL(target);
// Only render HTTP(S) pages. Other protocols are not valid web pages.
if (!["http:", "https:"].includes(targetUrl.protocol)) {
throw new Error("Only HTTP and HTTPS URLs are allowed");
}
if (!ALLOWED_HOSTNAMES.has(targetUrl.hostname)) {
throw new Error("This hostname is not allowed");
}
return targetUrl;
};
const renderHtml = async (env, targetUrl) => {
// The /content Quick Actions endpoint loads the page in Browser Run and returns
// a JSON envelope containing the rendered HTML in the result field.
const response = await env.BROWSER.quickAction("content", {
url: targetUrl.toString(),
gotoOptions: {
waitUntil: "networkidle2",
timeout: 30000,
},
// If your page has a specific readiness signal, use waitForSelector
// instead of relying only on network activity.
// waitForSelector: { selector: "[data-prerender-ready='true']", timeout: 30000 },
});
if (!response.ok) {
const detail = (await response.text()).slice(0, 500);
throw new Error(`Browser Run failed with ${response.status}: ${detail}`);
}
const data = await response.json();
if (!data.success || typeof data.result !== "string") {
throw new Error("Browser Run returned an unsuccessful response");
}
return data.result;
};
export default {
async fetch(request, env) {
try {
// Read and validate the URL before sending it to Browser Run.
const targetUrl = getTargetUrl(request);
const html = await renderHtml(env, targetUrl);
// Return the rendered HTML to the crawler or integration.
return new Response(html, {
headers: {
"content-type": "text/html; charset=utf-8",
},
});
} catch (error) {
return Response.json(
{ error: error instanceof Error ? error.message : "Unknown error" },
{ status: 400 },
);
}
},
};interface Env {
BROWSER: BrowserRun;
}
// Only render pages you control. This prevents the Worker from becoming
// an open browser-rendering proxy for arbitrary websites.
const ALLOWED_HOSTNAMES = new Set(["example.com", "www.example.com"]);
const getTargetUrl = (request: Request) => {
const requestUrl = new URL(request.url);
const target = requestUrl.searchParams.get("url");
if (!target) {
throw new Error("Missing url query parameter");
}
const targetUrl = new URL(target);
// Only render HTTP(S) pages. Other protocols are not valid web pages.
if (!["http:", "https:"].includes(targetUrl.protocol)) {
throw new Error("Only HTTP and HTTPS URLs are allowed");
}
if (!ALLOWED_HOSTNAMES.has(targetUrl.hostname)) {
throw new Error("This hostname is not allowed");
}
return targetUrl;
};
const renderHtml = async (env: Env, targetUrl: URL) => {
// The /content Quick Actions endpoint loads the page in Browser Run and returns
// a JSON envelope containing the rendered HTML in the result field.
const response = await env.BROWSER.quickAction("content", {
url: targetUrl.toString(),
gotoOptions: {
waitUntil: "networkidle2",
timeout: 30000,
},
// If your page has a specific readiness signal, use waitForSelector
// instead of relying only on network activity.
// waitForSelector: { selector: "[data-prerender-ready='true']", timeout: 30000 },
});
if (!response.ok) {
const detail = (await response.text()).slice(0, 500);
throw new Error(`Browser Run failed with ${response.status}: ${detail}`);
}
const data = (await response.json()) as {
success: boolean;
result?: string;
};
if (!data.success || typeof data.result !== "string") {
throw new Error("Browser Run returned an unsuccessful response");
}
return data.result;
};
export default {
async fetch(request, env): Promise<Response> {
try {
// Read and validate the URL before sending it to Browser Run.
const targetUrl = getTargetUrl(request);
const html = await renderHtml(env, targetUrl);
// Return the rendered HTML to the crawler or integration.
return new Response(html, {
headers: {
"content-type": "text/html; charset=utf-8",
},
});
} catch (error) {
return Response.json(
{ error: error instanceof Error ? error.message : "Unknown error" },
{ status: 400 },
);
}
},
} satisfies ExportedHandler<Env>;Worker 接受 url 查询参数,验证主机名,请求 Browser Run 渲染该 URL,并返回渲染后的 HTML。
waitUntil: "networkidle2" 选项等待页面在至少 500 毫秒内不超过两个网络连接。这通常足以处理客户端渲染的页面。如果页面需要更具体的就绪信号,向同一 Quick Actions payload 传递 waitForSelector,等待仅在内容加载后才出现的元素。更多信息请参阅 Browser Run Quick Actions 超时。
-
在 remote 模式下启动 Worker:
npx wrangler dev --remoteyarn wrangler dev --remotepnpm wrangler dev --remote.quickAction()方法尚不支持本地开发模式。在本地测试 Browser Run Quick Actions 时,请使用wrangler dev --remote。 -
在另一个终端中请求渲染后的页面:
curl "http://localhost:8787/?url=https://example.com/"响应应包含目标页面的渲染 HTML。
-
本地验证后部署 Worker:
npx wrangler deployyarn wrangler deploypnpm wrangler deploy -
部署后,从你的 Worker URL 请求渲染后的页面:
curl "https://<YOUR_WORKER_HOSTNAME>/?url=https://example.com/"
- 仅渲染你控制的主机名
- 需要边缘路由时,使用 Worker 作为第一接触点
- 仅对爬虫或集成请求调用 Browser Run
- 如果预期有重复的爬虫请求,缓存渲染后的 HTML
- 源内容变更时重新验证缓存的 HTML