Comparison ·

Playwright vs Puppeteer (2026): Browsers, Memory, Screenshots and Which to Pick

Use Playwright for end-to-end tests, Safari/WebKit coverage or a Python, Java or .NET codebase. Use Puppeteer for Chrome-focused Node.js scripts and screenshot or PDF workers, where its smaller client saves some memory per process. The two APIs look alike because they share a history: Playwright was started at Microsoft by engineers who had built Puppeteer at Google. Below is a side-by-side comparison with code and a reproducible RAM benchmark.

Bar chart of peak RAM on the same Chromium 156 build: Puppeteer 1,050 MB vs Playwright 1,075 MB in headless shell mode, 1,318 MB vs 1,412 MB in full headless Chrome

TL;DR

  • Pick Playwright for end-to-end tests, cross-browser coverage (Chromium, Firefox, WebKit), or a codebase in Python, Java or .NET.
  • Pick Puppeteer for Chrome-focused Node.js scripts, PDF and screenshot jobs, or when you want a smaller library with a slightly lower memory footprint (2 to 7% in our benchmark when both use the same browser build and mode).
  • Pick neither when screenshots are a product feature and you would rather not run a browser fleet. A screenshot API does that part.

Comparison table

 PlaywrightPuppeteer
MaintainerMicrosoftGoogle Chrome team
LanguagesJavaScript / TypeScript, Python, Java, .NETJavaScript / TypeScript only
BrowsersChromium, Firefox, WebKit (Safari engine)Chrome / Chromium, Firefox
ProtocolCDP for Chromium, patched builds for Firefox and WebKitCDP for Chrome, WebDriver BiDi for Firefox
Auto-waitingBuilt into locator actions; checks depend on the actionIn the Locator API (v20+); classic page.click() does not wait
Test runnerPlaywright Test includedNone; use Jest, Mocha or Vitest
Debugging toolsTrace Viewer, UI mode, codegenChrome DevTools, DevTools Recorder export
Screenshot extrasmask, animations, style, scalecaptureBeyondViewport, optimizeForSpeed
PDFChromium onlyChrome
Peak RAM, headless shell*1,075 MB1,050 MB
Peak RAM, full headless*1,412 MB1,318 MB

* Our benchmark on one machine: same Chromium 156 build for both, one page plus three full-page screenshots, median of 7 runs. Method, script and the default-settings caveat are in Memory usage.

Browsers and languages

This is the difference that most often decides the question. Playwright ships its own builds of Chromium, Firefox and WebKit and installs them with npx playwright install. WebKit is the reason many teams choose it: it is the closest you get to testing Safari on Linux CI.

Puppeteer downloads Chrome for Testing on npm install and supports Firefox through WebDriver BiDi. There is no WebKit support.

Languages are the second filter. Playwright has official packages for Python, Java and .NET with the same API shape. Puppeteer is Node.js only; the Python port pyppeteer is no longer maintained and its own README points to Playwright for Python. If your stack is Python, see five ways to take a website screenshot in Python.

API and auto-waiting

The same task in both libraries: open a page, fill a search box, click, wait for results.

// Playwright import { chromium } from "playwright"; const browser = await chromium.launch(); const page = await browser.newPage(); await page.goto("https://example.com"); await page.getByPlaceholder("Search").fill("pricing"); await page.getByRole("button", { name: "Search" }).click(); await page.locator(".results").waitFor(); await browser.close();
// Puppeteer import puppeteer from "puppeteer"; const browser = await puppeteer.launch(); const page = await browser.newPage(); await page.goto("https://example.com"); await page.locator("input[placeholder='Search']").fill("pricing"); await page.locator("button[type='submit']").click(); await page.waitForSelector(".results"); await browser.close();

With locators the two are close. Playwright goes further: before an action it waits for the checks that action needs. A click waits until the element is visible, stable, receives events and is enabled; fill() waits for visible, enabled and editable; a screenshot only for visible and stable. Role-based locators like getByRole() are first-class. In Puppeteer, older code written with page.click(selector) or page.$() does not wait at all, which is the source of many "works locally, flaky in CI" reports.

TaskPlaywrightPuppeteer
Isolated sessionbrowser.newContext()browser.createBrowserContext()
Block or mock requestspage.route()page.setRequestInterception(true) + request.abort()
Emulate a phonenewContext({ ...devices["iPhone 15"] })page.emulate(KnownDevices["iPhone 15"])
Run JS in the pagepage.evaluate()page.evaluate()
Talk CDP directlycontext.newCDPSession(page)page.createCDPSession()

Memory usage: Puppeteer is slightly lighter, with one catch

Memory matters once you run several browsers per server, so we measured it. To compare the libraries and not two browser versions, both drove the same Chromium 156 build that Playwright installs: its full Chromium for "full headless" and its chrome-headless-shell for "headless shell". Each run loaded only one library, launched the browser, opened a local 20-section page at 1440×900, took three full-page screenshots and recorded the peak resident memory of the Node.js process plus every browser process it started. Seven runs per mode, alternating the order.

ModeNode.js processBrowser processesTotal, medianTotal, range
Puppeteer, headless: "shell"234 MB819 MB1,050 MB1,016–1,070 MB
Playwright, default (headless shell)320 MB756 MB1,075 MB1,068–1,079 MB
Puppeteer, headless: true (full headless)162 MB1,154 MB1,318 MB1,295–1,358 MB
Playwright, channel: "chromium" (full headless)266 MB1,165 MB1,412 MB1,407–1,435 MB

With full headless Chrome, Puppeteer used about 95 MB (7%) less, and the ranges do not overlap. With the headless shell the gap was 25 MB (2%), close to run-to-run noise. The consistent difference is the library itself: the Node.js process with Puppeteer loaded was 85 to 105 MB smaller than with Playwright. On the browser side the numbers were close, and in shell mode Puppeteer's browser processes even used a little more, which points to different launch flags rather than a lighter engine.

The catch: the defaults are not the same. Puppeteer's headless: true launches full Chrome in new headless mode, while Playwright launches the smaller headless shell. With default settings, Playwright came out about 240 MB lighter in our runs (1,075 vs 1,318 MB). If memory matters with Puppeteer, choose the mode yourself.
// Puppeteer: the lightest setup for screenshots and PDFs const browser = await puppeteer.launch({ headless: "shell" });

Treat this as one experiment, not a universal rule. Setup: Apple M4 Max, macOS 27.2, Node.js 24.15, Puppeteer 24.42.0, Playwright 1.64.1, Chromium 156.0.8078.4. Resident memory summed across processes counts shared pages more than once, so the numbers are good for comparing modes, not for sizing servers. Heavy real-world pages move every row up, and results on Linux may differ.

Run the benchmark yourself

Install both libraries and Playwright's browsers, save the two files below in one folder, then run each mode several times:

npm i puppeteer playwright npx playwright install chromium chromium-headless-shell export CHROMIUM="$(node -e 'console.log(require("playwright").chromium.executablePath())')" # Playwright cache: ~/.cache/ms-playwright (Linux), ~/Library/Caches/ms-playwright (macOS) export HEADLESS_SHELL="$(find ~/.cache/ms-playwright ~/Library/Caches/ms-playwright -type f -name chrome-headless-shell 2>/dev/null | sort | tail -1)" for i in 1 2 3 4 5 6 7; do for m in pptr-shell pw-shell pptr-new pw-new; do node bench.mjs $m; done done

bench.mjs:

// Peak RAM of Puppeteer vs Playwright driving the SAME Chromium build. // Usage: CHROMIUM=... HEADLESS_SHELL=... node bench.mjs <pptr-shell|pw-shell|pptr-new|pw-new> // CHROMIUM and HEADLESS_SHELL are the binaries Playwright installed, so both // libraries drive the same browser build. Only one library is loaded per run. import { execSync } from "node:child_process"; const mode = process.argv[2]; const url = new URL("./page.html", import.meta.url).href; const { CHROMIUM, HEADLESS_SHELL } = process.env; // Resident memory of this Node.js process and every process it started. function treeRss() { const rows = execSync("ps -A -o pid=,ppid=,rss=").toString().trim().split("\n") .map((l) => l.trim().split(/\s+/).map(Number)); const kids = new Map(), rss = new Map(); for (const [pid, ppid, r] of rows) { rss.set(pid, r); if (!kids.has(ppid)) kids.set(ppid, []); kids.get(ppid).push(pid); } let node = rss.get(process.pid) || 0, browser = 0; const stack = [...(kids.get(process.pid) || [])]; while (stack.length) { const p = stack.pop(); browser += rss.get(p) || 0; stack.push(...(kids.get(p) || [])); } return { node, browser, total: node + browser }; } let peak = { total: 0 }; const sample = () => { const r = treeRss(); if (r.total > peak.total) peak = r; }; const timer = setInterval(sample, 50); let browser, page; if (mode.startsWith("pptr")) { const { default: puppeteer } = await import("puppeteer"); browser = await puppeteer.launch(mode === "pptr-shell" ? { headless: "shell", executablePath: HEADLESS_SHELL } : { headless: true, executablePath: CHROMIUM }); page = await browser.newPage(); await page.setViewport({ width: 1440, height: 900 }); } else { const { chromium } = await import("playwright"); browser = await chromium.launch(mode === "pw-new" ? { channel: "chromium" } : {}); page = await browser.newPage({ viewport: { width: 1440, height: 900 } }); } await page.goto(url, { waitUntil: "load" }); for (let i = 0; i < 3; i++) await page.screenshot({ fullPage: true }); await new Promise((r) => setTimeout(r, 300)); sample(); clearInterval(timer); const version = await browser.version(); await browser.close(); const mb = (kb) => Math.round(kb / 1024); console.log(JSON.stringify({ mode, version, nodeMB: mb(peak.node), browserMB: mb(peak.browser), totalMB: mb(peak.total) }));

page.html:

<!doctype html><html><head><style>body{font-family:system-ui;margin:0}section{height:600px;padding:40px;background:linear-gradient(135deg,#0ea5e9,#6366f1);color:#fff;border-bottom:4px solid #fff}</style></head><body> <script>for(let i=0;i<20;i++)document.write('<section><h1>Section '+i+'</h1><p>'+'Lorem ipsum dolor sit amet. '.repeat(80)+'</p></section>')</script></body></html>

The ps call works on macOS and Linux. Each run prints one JSON line; compare medians, not single runs.

Screenshots and PDF

A full-page screenshot looks almost identical in both:

// Playwright await page.screenshot({ path: "page.png", fullPage: true }); // Puppeteer await page.screenshot({ path: "page.png", fullPage: true });

The differences show up when you need clean, repeatable images:

  • Hiding dynamic content. Playwright has mask (cover elements with a box) and style (CSS applied only during the capture). In Puppeteer you inject a style tag yourself with page.addStyleTag().
  • Animations. Playwright's animations: "disabled" fast-forwards CSS animations. Puppeteer has no equivalent option.
  • Element screenshots. Playwright: page.locator(".hero").screenshot(). Puppeteer: (await page.waitForSelector(".hero")).screenshot(), because its Locator class has no screenshot method.
  • Speed options. Puppeteer has optimizeForSpeed for faster encoding and captureBeyondViewport to control how full-page captures are rendered.

For PDF, both call Chrome's print engine through page.pdf() with nearly the same options. Playwright's PDF works only in Chromium, so cross-browser support does not help here.

Every option for each library is covered in our Playwright screenshot guide and Puppeteer screenshot guide. If Puppeteer returns blank or cut-off images, see nine fixes for blank Puppeteer screenshots.

Testing

This is where Playwright has the clearest lead. Playwright Test comes with parallel workers, retries, fixtures, web-first assertions such as await expect(locator).toBeVisible(), an HTML report, trace recordings you can step through, and visual comparisons with toHaveScreenshot().

// Playwright Test import { test, expect } from "@playwright/test"; test("pricing page shows plans", async ({ page }) => { await page.goto("/pricing"); await expect(page.getByRole("heading", { name: "Pricing" })).toBeVisible(); await expect(page).toHaveScreenshot(); });

Puppeteer is a library, not a test framework. You combine it with Jest or Mocha and add assertions, retries and screenshot diffing yourself. That is fine for a few smoke tests but adds up for a large suite. Related guides: screenshots on test failure and visual regression testing.

Automation and scraping

For scripts that run outside a test suite, such as generating reports, filling internal forms or collecting public data, both work well. Puppeteer has the longer history here, so you will find more examples and plugins for it. Its smaller Node.js client, about 100 MB less per process in our benchmark, also helps when a server runs many worker processes in parallel.

Playwright's advantages for automation are browser contexts that are cheap to create, page.route() for blocking images and trackers, and the same code in Python if your data team works there. Whichever you pick, respect the site's terms and robots.txt and keep request rates reasonable.

AI agents and MCP

Both have official MCP servers that let coding agents control a browser. Microsoft's Playwright MCP works from the accessibility tree and can take screenshots on request. Google's Chrome DevTools MCP is built on Puppeteer and adds performance traces and network inspection. For details on the Playwright side, see Playwright MCP screenshots.

Which to pick

Your situationPick
End-to-end tests for a web appPlaywright
You need to test Safari / WebKitPlaywright
Python, Java or .NET codebasePlaywright
Chrome-only Node.js script or workerPuppeteer
Many Node.js worker processes on a tight RAM budgetPuppeteer, with the headless mode set explicitly (small gain, measure on your pages)
Chrome-specific work: DevTools performance traces, Chrome DevTools MCP, deep CDP usePuppeteer
Testing a Chrome extensionEither; Playwright supports it in Chromium via launchPersistentContext()
Existing Puppeteer code that worksKeep Puppeteer; migrating is rarely worth it on its own
Screenshots or PDFs as a product feature, at volumeNeither; see below

When to use neither

If what you need is images of web pages for link previews, archives, reports or customer-facing features, the library choice is the smaller decision. The larger one is running browsers in production: memory limits, crashed processes, fonts in Docker, queueing and retries. The memory table above is for a single simple page; multiply it by your concurrency.

A screenshot API takes that work off your servers. One request replaces the launch, wait and capture code from either library:

curl --get "https://api.snapshotflow.com/screenshot" \ -H "X-Api-Key: $SNAPSHOTFLOW_API_KEY" \ --data-urlencode "url=https://example.com" \ --data-urlencode "full_page=true" \ --data-urlencode "width=1440" \ --data-urlencode "format=webp" \ --data-urlencode "block_cookie_banners=true" \ -o page.webp

Option-by-option migration tables and cost comparisons are in Playwright alternative for screenshots and Puppeteer alternative for screenshots.

FAQ

Is Playwright faster than Puppeteer?

For Chromium they drive the same engine over the same protocol, so single actions take about the same time. Playwright test suites often finish sooner because the runner parallelises tests and auto-waiting removes fixed sleeps. Benchmarks that show one library much faster usually compare different headless modes or waiting strategies.

Does Puppeteer use less memory than Playwright?

Slightly, in our benchmark on the same Chromium build: about 7% less with full headless Chrome and 2% less with the headless shell, mostly because the Puppeteer client in Node.js is smaller. With default settings Puppeteer uses more, because it launches full headless Chrome while Playwright launches the headless shell. Use headless: "shell" to get the lower number, and measure on your own pages.

Can I migrate from Puppeteer to Playwright?

Yes. Most calls map directly: puppeteer.launch() to chromium.launch(), page.waitForSelector() to locator.waitFor(), setRequestInterception() to page.route(). The main work is replacing manual waits with locators.

Is Puppeteer still maintained?

Yes. The Chrome team releases it regularly alongside Chrome, and it gained stable Firefox support through WebDriver BiDi.

What about Selenium?

Selenium supports the most languages and browsers through WebDriver and is common in large enterprise suites. For new projects, Playwright usually needs less setup. See our Selenium screenshot guide for how it compares on captures.

Sources and further reading

Try it: 300 free screenshots, no card

Keep Playwright or Puppeteer for your tests. For screenshots your product depends on, send one HTTP request and get the image back, with full page, retina, WebP and cookie-banner blocking built in.