このガイドに沿って進めると、Durable Objects と Browser Run API を使い、Web ページのスクリーンショットを撮影して R2 に保存する Worker を作成できます。
Durable Objects でブラウザーセッションを維持すると、新しいセッションの起動時間を省けてパフォーマンスが上がります。Durable Objects はセッションを再利用するため、必要な同時セッション数も減ります。
- Cloudflare アカウント ↗ に登録します。
Node.js↗ をインストールします。
Node.js のバージョンマネージャー
権限の問題を避け、Node.js のバージョンを切り替えられるよう、Volta ↗ や nvm ↗ などの Node バージョンマネージャーを使います。このガイドの後半で説明する Wrangler には、Node バージョン 16.17.0 以降が必要です。
Cloudflare Workers は、インフラの設定や運用なしに、新しいアプリケーションの作成や既存アプリの拡張ができるサーバーレス実行環境です。Worker アプリケーションは、ヘッドレスブラウザーとやり取りしてスクリーンショット撮影などの操作を行うコンテナになります。
次のコマンドで、browser-worker という名前の新しい Worker プロジェクトを作成します。
npm create cloudflare@latest -- browser-workeryarn create cloudflare browser-workerpnpm create cloudflare@latest browser-workerbrowser-worker ディレクトリで、Cloudflare の Puppeteer フォーク をインストールします。
npm i -D @cloudflare/puppeteeryarn add -D @cloudflare/puppeteerpnpm add -D @cloudflare/puppeteerbun add -d @cloudflare/puppeteer本番用と開発用の 2 つの R2 バケットを作成します。
バケット名は小文字のみで、使える記号はハイフンだけです。
wrangler r2 bucket create screenshots
wrangler r2 bucket create screenshots-test作成できたか確認するには、次を実行します。
wrangler r2 bucket listlist コマンドを実行すると、作成したバケットを含む、すべてのバケット名が表示されます。
browser-worker プロジェクトの Wrangler 設定ファイル に、ブラウザー バインディング と Node.js 互換フラグ を追加します。ブラウザーバインディングは Worker とヘッドレスブラウザーの通信を可能にし、スクリーンショット撮影、PDF 生成などの操作ができます。
Wrangler 設定ファイルを、Browser Run API バインディング、作成した R2 バケット、Durable Object で更新します。
{
"$schema": "./node_modules/wrangler/config-schema.json",
"name": "rendering-api-demo",
"main": "src/index.js",
// Set this to today's date
"compatibility_date": "2026-09-20",
"compatibility_flags": ["nodejs_compat"],
"account_id": "<ACCOUNT_ID>",
// Browser Run API binding
"browser": {
"binding": "MYBROWSER",
},
// Bind an R2 Bucket
"r2_buckets": [
{
"binding": "BUCKET",
"bucket_name": "screenshots",
"preview_bucket_name": "screenshots-test",
},
],
// Binding to a Durable Object
"durable_objects": {
"bindings": [
{
"name": "BROWSER",
"class_name": "Browser",
},
],
},
"migrations": [
{
"tag": "v1", // Should be unique for each entry
"new_sqlite_classes": [
// Array of new classes
"Browser",
],
},
],
}"$schema" = "./node_modules/wrangler/config-schema.json"
name = "rendering-api-demo"
main = "src/index.js"
# Set this to today's date
compatibility_date = "2026-09-20"
compatibility_flags = [ "nodejs_compat" ]
account_id = "<ACCOUNT_ID>"
[browser]
binding = "MYBROWSER"
[[r2_buckets]]
binding = "BUCKET"
bucket_name = "screenshots"
preview_bucket_name = "screenshots-test"
[[durable_objects.bindings]]
name = "BROWSER"
class_name = "Browser"
[[migrations]]
tag = "v1"
new_sqlite_classes = [ "Browser" ]次のコードは Durable Object を使い、Puppeteer でブラウザーを起動します。解像度を変えて一連の Web ページを開き、それぞれスクリーンショットを撮影して R2 にアップロードします。
Durable Object は、最後の使用から 60 秒間ブラウザーセッションを開いたままにします。セッションが開いていれば、新しいリクエストは新規作成せず既存セッションを再利用します。次のコードをコピーして Worker に貼り付けます。
import { DurableObject } from "cloudflare:workers";
import * as puppeteer from "@cloudflare/puppeteer";
export default {
async fetch(request, env) {
const obj = env.BROWSER.getByName("browser");
// Send a request to the Durable Object, then await its response
const resp = await obj.fetch(request);
return resp;
},
};
const KEEP_BROWSER_ALIVE_IN_SECONDS = 60;
export class Browser extends DurableObject {
browser;
keptAliveInSeconds = 0;
storage;
constructor(state, env) {
super(state, env);
this.storage = state.storage;
}
async fetch(request) {
// Screen resolutions to test out
const width = [1920, 1366, 1536, 360, 414];
const height = [1080, 768, 864, 640, 896];
// Use the current date and time to create a folder structure for R2
const nowDate = new Date();
const coeff = 1000 * 60 * 5;
const roundedDate = new Date(
Math.round(nowDate.getTime() / coeff) * coeff,
).toString();
const folder = roundedDate.split(" GMT")[0];
// If there is a browser session open, re-use it
if (!this.browser || !this.browser.isConnected()) {
console.log(`Browser DO: Starting new instance`);
try {
this.browser = await puppeteer.launch(this.env.MYBROWSER);
} catch (e) {
console.log(
`Browser DO: Could not start browser instance. Error: ${e}`,
);
}
}
// Reset keptAlive after each call to the DO
this.keptAliveInSeconds = 0;
// Check if browser exists before opening page
if (!this.browser)
return new Response("Browser launch failed", { status: 500 });
const page = await this.browser.newPage();
// Take screenshots of each screen size
for (let i = 0; i < width.length; i++) {
await page.setViewport({ width: width[i], height: height[i] });
await page.goto("https://workers.cloudflare.com/");
const fileName = `screenshot_${width[i]}x${height[i]}`;
const sc = await page.screenshot();
await this.env.BUCKET.put(`${folder}/${fileName}.jpg`, sc);
}
// Close tab when there is no more work to be done on the page
await page.close();
// Reset keptAlive after performing tasks to the DO
this.keptAliveInSeconds = 0;
// Set the first alarm to keep DO alive
const currentAlarm = await this.storage.getAlarm();
if (currentAlarm == null) {
console.log(`Browser DO: setting alarm`);
const TEN_SECONDS = 10 * 1000;
await this.storage.setAlarm(Date.now() + TEN_SECONDS);
}
return new Response("success");
}
async alarm() {
this.keptAliveInSeconds += 10;
// Extend browser DO life
if (this.keptAliveInSeconds < KEEP_BROWSER_ALIVE_IN_SECONDS) {
console.log(
`Browser DO: has been kept alive for ${this.keptAliveInSeconds} seconds. Extending lifespan.`,
);
await this.storage.setAlarm(Date.now() + 10 * 1000);
// You can ensure the ws connection is kept alive by requesting something
// or just let it close automatically when there is no work to be done
// for example, `await this.browser.version()`
} else {
console.log(
`Browser DO: exceeded life of ${KEEP_BROWSER_ALIVE_IN_SECONDS}s.`,
);
if (this.browser) {
console.log(`Closing browser.`);
await this.browser.close();
}
}
}
}import { DurableObject } from "cloudflare:workers";
import * as puppeteer from "@cloudflare/puppeteer";
interface Env {
MYBROWSER: Fetcher;
BUCKET: R2Bucket;
BROWSER: DurableObjectNamespace;
}
export default {
async fetch(request, env): Promise<Response> {
const obj = env.BROWSER.getByName("browser");
// Send a request to the Durable Object, then await its response
const resp = await obj.fetch(request);
return resp;
},
} satisfies ExportedHandler<Env>;
const KEEP_BROWSER_ALIVE_IN_SECONDS = 60;
export class Browser extends DurableObject<Env> {
private browser?: puppeteer.Browser;
private keptAliveInSeconds: number = 0;
private storage: DurableObjectStorage;
constructor(state: DurableObjectState, env: Env) {
super(state, env);
this.storage = state.storage;
}
async fetch(request: Request): Promise<Response> {
// Screen resolutions to test out
const width: number[] = [1920, 1366, 1536, 360, 414];
const height: number[] = [1080, 768, 864, 640, 896];
// Use the current date and time to create a folder structure for R2
const nowDate = new Date();
const coeff = 1000 * 60 * 5;
const roundedDate = new Date(
Math.round(nowDate.getTime() / coeff) * coeff,
).toString();
const folder = roundedDate.split(" GMT")[0];
// If there is a browser session open, re-use it
if (!this.browser || !this.browser.isConnected()) {
console.log(`Browser DO: Starting new instance`);
try {
this.browser = await puppeteer.launch(this.env.MYBROWSER);
} catch (e) {
console.log(
`Browser DO: Could not start browser instance. Error: ${e}`,
);
}
}
// Reset keptAlive after each call to the DO
this.keptAliveInSeconds = 0;
// Check if browser exists before opening page
if (!this.browser)
return new Response("Browser launch failed", { status: 500 });
const page = await this.browser.newPage();
// Take screenshots of each screen size
for (let i = 0; i < width.length; i++) {
await page.setViewport({ width: width[i], height: height[i] });
await page.goto("https://workers.cloudflare.com/");
const fileName = `screenshot_${width[i]}x${height[i]}`;
const sc = await page.screenshot();
await this.env.BUCKET.put(`${folder}/${fileName}.jpg`, sc);
}
// Close tab when there is no more work to be done on the page
await page.close();
// Reset keptAlive after performing tasks to the DO
this.keptAliveInSeconds = 0;
// Set the first alarm to keep DO alive
const currentAlarm = await this.storage.getAlarm();
if (currentAlarm == null) {
console.log(`Browser DO: setting alarm`);
const TEN_SECONDS = 10 * 1000;
await this.storage.setAlarm(Date.now() + TEN_SECONDS);
}
return new Response("success");
}
async alarm(): Promise<void> {
this.keptAliveInSeconds += 10;
// Extend browser DO life
if (this.keptAliveInSeconds < KEEP_BROWSER_ALIVE_IN_SECONDS) {
console.log(
`Browser DO: has been kept alive for ${this.keptAliveInSeconds} seconds. Extending lifespan.`,
);
await this.storage.setAlarm(Date.now() + 10 * 1000);
// You can ensure the ws connection is kept alive by requesting something
// or just let it close automatically when there is no work to be done
// for example, `await this.browser.version()`
} else {
console.log(
`Browser DO: exceeded life of ${KEEP_BROWSER_ALIVE_IN_SECONDS}s.`,
);
if (this.browser) {
console.log(`Closing browser.`);
await this.browser.close();
}
}
}
}npx wrangler dev を実行して、Worker をローカルでテストします。
npx wrangler deploy を実行して、Worker を Cloudflare のグローバルネットワークにデプロイします。
- その他の Puppeteer の例 ↗
- Durable Objects を始める
- Workers から R2 を使う