1---
2title: Browser rendering
3description: Attach Cloudflare Browser Rendering to a Worker — run quick actions like markdown and scrape, stream screenshots and PDFs, or drive the raw binding with puppeteer.
4sidebar:
5 order: 22
6---
7
8Browser Rendering gives a Worker a headless browser. In alchemy, the
9`Cloudflare.Browser` binding attaches it at deploy time and hands you
10an Effect-native client: JSON quick actions (`markdown`, `content`,
11`links`, `scrape`, `snapshot`, `json`) resolve to their parsed
12payloads, binary actions (`screenshot`, `pdf`) resolve to byte
13streams, and the raw binding is one accessor away for
14`@cloudflare/puppeteer`.
15
16## Bind the browser to a Worker
17
18`yield*` the binding in the Worker's Construction phase and provide
19`BrowserBinding` as a layer:
20
21```typescript
22// src/worker.ts
23import * as Cloudflare from "alchemy/Cloudflare";
24import * as Effect from "effect/Effect";
25import * as HttpServerResponse from "effect/http/HttpServerResponse";
26
27export default Cloudflare.Worker(
28 "BrowserWorker",
29 { main: import.meta.url },
30 Effect.gen(function* () {
31 const browser = yield* Cloudflare.Browser("BROWSER");
32
33 return {
34 fetch: Effect.gen(function* () {
35 const markdown = yield* browser
36 .markdown({ url: "https://example.com" })
37 .pipe(Effect.orDie);
38 return yield* HttpServerResponse.json({
39 markdown: markdown.result,
40 });
41 }),
42 };
43 }).pipe(Effect.provide(Cloudflare.Workers.BrowserBinding)),
44);
45```
46
47`Cloudflare.Browser("BROWSER")` registers the binding on the Worker at
48deploy time and returns the typed `BrowserClient` for runtime use.
49`markdown` renders the page in a headless browser and resolves to the
50converted Markdown; failures surface as a typed `BrowserError`.
51
52## Scrape elements by selector
53
54The other JSON quick actions follow the same shape. Pass a URL plus
55action-specific options and get the parsed result back:
56
57```typescript
58const scrape = yield* browser.scrape({
59 url: "https://example.com",
60 elements: [{ selector: "h1" }],
61});
62
63const heading = scrape.result[0]?.results[0]?.text;
64// "Example Domain"
65```
66
67`content` returns the rendered HTML, `links` every link on the page,
68`snapshot` HTML plus a base64 screenshot in one call, and `json`
69extracts structured data from the page with an AI `prompt`.
70
71## Stream a screenshot or PDF
72
73`screenshot` and `pdf` are binary actions, so they return a
74`Stream` of `Uint8Array` chunks instead of a parsed payload:
75
76```typescript
77import * as Stream from "effect/Stream";
78
79const chunks = yield* browser
80 .screenshot({ url: "https://example.com" })
81 .pipe(Stream.runCollect, Effect.orDie);
82```
83
84`Stream.runCollect` drains the response into memory; for large PDFs
85you can just as well pipe the stream onward without buffering it.
86
87## Declare the binding as env metadata
88
89Workers whose `main` module exports a plain `fetch` handler declare
90the binding on `env` instead:
91
92```typescript
93// alchemy.run.ts
94export const Worker = Cloudflare.Worker("PuppeteerWorker", {
95 main: "./src/worker.ts",
96 compatibility: { flags: ["nodejs_compat"] },
97 env: { BROWSER: Cloudflare.Browser("BROWSER") },
98});
99
100export type WorkerEnv = Cloudflare.InferEnv<typeof Worker>;
101// { BROWSER: BrowserRun }
102```
103
104`InferEnv` types the `env` your handler receives. `BROWSER` flows
105through as Cloudflare's native `BrowserRun` binding. The
106`nodejs_compat` flag is required for `@cloudflare/puppeteer`.
107
108## Drive it with puppeteer
109
110Launch a full browser session against the binding when quick actions
111aren't enough:
112
113```typescript
114// src/worker.ts
115import puppeteer from "@cloudflare/puppeteer";
116import type { WorkerEnv } from "../alchemy.run.ts";
117
118export default {
119 async fetch(request: Request, env: WorkerEnv): Promise<Response> {
120 const browser = await puppeteer.launch(env.BROWSER as any);
121 try {
122 const page = await browser.newPage();
123 await page.goto("https://example.com", { waitUntil: "networkidle0" });
124 return Response.json({ title: await page.title() });
125 } finally {
126 await browser.close();
127 }
128 },
129};
130```
131
132The same escape hatch exists inside an Effect-style Worker:
133`yield* browser.raw` resolves to the underlying `BrowserRun` binding,
134ready to hand to promise-shaped libraries.
135
136## Where next
137
138- [Workers](/cloudflare/compute/workers) — the two-phase Worker model the
139 binding lives inside.
140- [Browser API reference](/providers/cloudflare/workers/browser) —
141 every quick action, option type, and result shape.