Skip to main content
Tutorial

Migrate Puppeteer/Playwright to a Scraping API

Migrate Puppeteer or Playwright scrapers to a web scraping API. Map page.goto, waitForSelector, and evaluate to /v1/scrape — plus what an API can't replace.

fastcrw
By RecepJuly 6, 20269 min readLast updated: July 12, 2026

By the fastCRW team · Credit costs and capabilities verified 2026-05-18 · Verify independently before relying on any number.

Disclosure: We build fastCRW, so weight this accordingly. This guide is written to be useful even if you stay on Puppeteer or Playwright for part of your workload — and we say plainly, below, where the browser stays the right tool.

Why migrate Puppeteer/Playwright to a scraping API

If you maintain Node-based Puppeteer or Playwright scrapers, you already know the tax. A headless Chrome context is heavy — roughly 1–2 GB of RAM per live context once a real page loads its JavaScript, fonts, and images — so a fleet that does dozens of pages in parallel turns into a memory and orchestration problem. On top of that you carry the maintenance: selectors drift, sites change layout, headless-detection scripts trip, and your "scraper" slowly becomes a browser-babysitting service.

The uncomfortable truth is that most scrapes do not need a full browser session. They need the clean text or a few structured fields off a page that happens to render with JavaScript. For that case, you can migrate Puppeteer to a scraping API: hand the URL to a managed endpoint, let it run the renderer, and get back Markdown or JSON. You drop the ~1–2 GB-per-context browser fleet and replace it with one HTTP call. fastCRW's engine is a single static Rust binary — a ~8 MB image, one container (a structural fact from the open-core README, not a benchmark) — versus the multi-gigabyte browser stack you operate today.

This post maps the Puppeteer/Playwright API surface onto fastCRW's /v1/scrape, shows a before/after port, and is explicit about the three things the API does not replace so you migrate the right scrapes and keep the browser for the rest.

Mapping the API surface

Most of a Puppeteer or Playwright scraper is glue: launch a browser, navigate, wait for content, pull HTML or evaluate selectors, then clean the result. A scraping API collapses that glue into request parameters. Here is the function-by-function mapping.

Puppeteer / PlaywrightfastCRW equivalent
browser.newPage() + page.goto(url)One POST /v1/scrape with { url }
page.content() + an HTML-to-text/Markdown stepformats: ["markdown"] (the default) returns clean Markdown
page.$eval() / page.evaluate() to pull fieldsformats: ["json"] + a jsonSchema for the fields
waitForSelector() / waitUntil: "networkidle"The chrome renderer executes JS; auto decides per page
Managing your own headless Chrome poolThe managed renderer; you never launch a browser

A few notes that matter for accuracy. fastCRW returns Markdown by default from /v1/scrape, so the entire "get HTML, then run Turndown/Readability to strip nav and ads" stage of your scraper disappears. For fields, you describe what you want with a jsonSchema and the engine fills it using a managed LLM — LLM-based JSON extraction is a managed feature available on paid plans (the FREE plan has no LLM features). The renderer choice (auto, http, lightpanda, chrome) is the lever that replaces your waitForSelector logic: auto tries fast paths first and falls back chrome → lightpanda → http, while chrome forces a full JavaScript render for the pages that genuinely need it.

Porting a script, concretely

Here is a representative Playwright scraper that grabs a product title and price from a JS-rendered page:

const browser = await chromium.launch();const page = await browser.newPage();await page.goto(url, { waitUntil: "networkidle" });const title = await page.$eval("h1", el => el.textContent);const price = await page.$eval(".price", el => el.textContent);await browser.close();

That is a browser launch, a navigation, a network-idle wait, two brittle selectors, and teardown — plus whatever retry and memory handling you bolted on around it. The fastCRW equivalent is one request that asks for typed fields:

POST /v1/scrape with body { "url": "https://example.com/product/42", "formats": ["json"], "jsonSchema": { "type": "object", "properties": { "title": { "type": "string" }, "price": { "type": "number" } } } }

The response carries title and price as typed fields. No selectors to maintain — if the site moves the price into a different element, schema-guided extraction usually still finds it, where a CSS selector would silently break.

Keep your Node code: a base-URL swap

You do not need a new SDK. fastCRW exposes a Firecrawl-compatible REST API, so if you already use the Firecrawl SDK (or are willing to adopt it), the migration is a drop-in after a base-URL swap — point api_url at your fastCRW instance and existing client code keeps working. That keeps the diff small: you are replacing the browser-driving module, not rewriting your whole pipeline. (A short known list of field-name and error-envelope differences exists; validate those before cutover — see our Firecrawl migration guide.)

Choosing http vs chrome — and the credit cost

Renderer choice affects quality and speed, but not credit cost. Every renderer — http, lightpanda, chrome, and auto — costs 1 credit for a Markdown scrape. There is no chrome surcharge. A static page and a fully JS-rendered page cost the same 1 credit; you pick the renderer based on what the page needs, not to manage cost.

One important caveat: the typed-field extraction the porting example above uses — any request with formats: ["json"] + a jsonSchema — is the 1-credit scrape plus the LLM token cost, billed as usage-metered LLM credits, because it runs an LLM over the page. That extra cost is the same regardless of renderer, and it scales with page size and token usage rather than being a fixed multiple. So budget by output, not renderer: a plain Markdown scrape is a flat 1 credit, while structured extraction adds usage-metered LLM credits on top. If you only need clean text, stay on formats: ["markdown"] and keep the flat 1-credit cost. For the broader cost picture and current tier pricing, see /pricing.

What stays in Puppeteer/Playwright

An API does not replace a browser for every job, and pretending otherwise would cost you a bad migration. Three limits decide what stays:

  • Clicks, form fills, and logins. fastCRW is stateless per request — there is no persistent session that survives between calls, so multi-step flows (log in, navigate a wizard, submit a form, then read the result) are exactly what a real browser is for. If your scraper drives an interaction, keep it in Puppeteer/Playwright.
  • Screenshots. fastCRW has no screenshot output — a request for formats: ["screenshot"] returns HTTP 422. If you rely on Puppeteer's page.screenshot() for visual diffs, PDFs of rendered pages, or image capture, that workload stays in the browser.
  • Heavily protected sites. fastCRW has no built-in anti-bot / Fire-engine. The chrome renderer handles normal JS rendering, but it is not a managed proxy-rotation and fingerprint-evasion layer. Sites with aggressive bot defenses may still need your hardened browser setup or a dedicated proxy service.

State these to yourself honestly before you migrate a scraper, not after. A read-only scrape of a content or product page is an excellent API candidate; a login-walled, screenshot-producing, anti-bot-fighting flow is not.

A hybrid migration strategy

The pragmatic move is not "rip out Puppeteer." It is to split your scrapers by what they actually do:

  1. Inventory by interaction. Tag each scraper as read-only (navigate + extract) or interactive (clicks, forms, logins, screenshots).
  2. Migrate the read-only set first. These are the majority for most teams, and they are the ones eating browser RAM for no reason. Move them to /v1/scrape via the Firecrawl-compatible base-URL swap. This is where you reclaim the ~1–2 GB-per-context fleet cost and shrink to a single ~8 MB binary or managed credits — the win that low-memory scraping is built around.
  3. Keep the interactive set in the browser. Let Puppeteer/Playwright do what only a stateful browser can. Many teams find the surviving browser fleet shrinks to a fraction of its former size.
  4. Measure before you fully commit. Run both paths on identical URLs for a couple of weeks and compare content parity, p50/p90 latency, and cost. Extraction quality is downstream of getting clean content out of the page in the first place — on Firecrawl's public 819-URL labeled dataset, fastCRW recorded the highest truth-recall of the three tools tested at 63.74%, with p50 1914 ms beating Firecrawl's 2305 ms (diagnose_3way.py, 2026-05-08). In fast mode, fastCRW's p90 is 4348 ms — the lowest of the three (Crawl4AI 4754 ms, Firecrawl 6937 ms) — because the chrome-stealth fallback that recovers pages the others miss is what earns both the recall lead and the strong fast-mode tail. See /benchmarks.

A hybrid migration gives you the footprint win on the bulk of your scrapes without forcing the genuinely interactive ones through an API that was never meant to drive a browser session.

Where Puppeteer/Playwright genuinely win

To keep this honest: for stateful, multi-step automation — logging in, filling and submitting forms, clicking through paginated UIs that only respond to real events, and capturing screenshots or PDFs — Puppeteer and Playwright are the right tool and a scraping API is not a substitute. They also give you fine-grained, per-step control (intercept requests, mock responses, throttle network) that a single stateless call cannot express. If that is the core of your scraper, migrating it to an API would be a downgrade. For a deeper feature-by-feature look, see Playwright vs Puppeteer vs CRW and Playwright as a scraper vs fastCRW.

Sources

  • fastCRW canonical facts (credits, renderers, gaps, footprint): github.com/us/crw open-core README and
  • Scrape benchmark of record — diagnose_3way.py, Firecrawl public dataset, 819 labeled URLs, 2026-05-08 (truth-recall 63.74%, p50 1914 ms, p90 fast-mode 4348 ms)
  • Live pricing and credit costs: /pricing · benchmark detail: /benchmarks

Related: Playwright as a scraper vs fastCRW · Playwright vs Puppeteer vs CRW · Migrate Selenium to a scraping API · Low-memory scraping

FAQ

Frequently asked questions

How do I migrate Puppeteer or Playwright scrapers to an API?
Map each function to a /v1/scrape parameter: page.goto plus page.content() becomes a single scrape returning Markdown (the default), page.$eval/evaluate becomes a jsonSchema for typed field extraction, and waitForSelector/networkidle becomes the chrome renderer (auto picks it per page). Because fastCRW is Firecrawl-compatible, you keep your Node code and migrate by swapping the api_url base URL. Move read-only scrapes first; keep interactive flows in the browser.
Can a scraping API replace headless Chrome for all my scrapes?
No, and you shouldn't expect it to. A scraping API replaces the read-only majority — navigate a page, get clean content or fields. It does not replace a browser for clicks, form fills, and logins (fastCRW is stateless per request), for screenshots (formats:['screenshot'] returns HTTP 422), or for heavily protected sites (fastCRW has no built-in anti-bot / Fire-engine). Use a hybrid split: API for read-only scrapes, browser for stateful interaction.
How much RAM does a Puppeteer browser context use vs an API call?
A live Puppeteer or Playwright Chrome context typically uses roughly 1–2 GB of RAM once a real page loads its JavaScript, fonts, and images, and that cost multiplies with parallel contexts. An API call uses none of your RAM — the rendering happens on the managed engine. fastCRW's engine itself is a single static Rust binary in a ~8 MB image running as one container (a structural fact), so self-hosting it is far lighter than operating a browser fleet.
Does fastCRW support clicks, logins, and form fills?
No. fastCRW is stateless per request, so there is no persistent session to carry a login or a multi-step form across calls. Interactions like clicking through a wizard, submitting a form, and then reading the result are exactly what Puppeteer or Playwright are for — keep those flows in the browser and migrate only your read-only scrapes to the API.
Can fastCRW take screenshots like Puppeteer?
No. Screenshot output is not supported — a request with formats:['screenshot'] returns HTTP 422. If your Puppeteer scraper relies on page.screenshot() for visual diffs, image capture, or PDFs of rendered pages, that workload stays in the browser. fastCRW returns Markdown by default and structured JSON via a jsonSchema, but not images.

Get Started

Try fastCRW free

Run a live request in the playground — no signup required. Or grab a free API key with 500 credits, no credit card.

Continue exploring

More tutorial posts

View category archive