Puppeteer vs Playwright 2026: Compared
Puppeteer vs Playwright is the pick most Node devs face when they need to drive a browser in 2026. Puppeteer is the lean Chrome-only library from Google. Playwright is the full cross-browser test kit from Microsoft. Both work. They are not the same fit for every job.
If you only need to drive Chrome for scraping, PDFs, or quick screenshots, Puppeteer is smaller, simpler, and faster to learn. If you need to test a real product across Chrome, Firefox, and Safari with auto-waits, retries, and trace files, Playwright wins on day one.
This guide pits both libs against each other across browser support, API design, test runner, speed, language reach, and CI cost. Code samples for the same five jobs let you see the gap.
TL;DR: Which one should you pick?
Skip the deep dive? Here is the short answer.
| Dimension | Puppeteer | Playwright | Winner |
|---|---|---|---|
| Browser support | Chrome, Firefox (beta) | Chromium, Firefox, WebKit | Playwright |
| Languages | JS, TS only | JS, TS, Python, Java, .NET | Playwright |
| Test runner | None (BYO Jest/Mocha) | Built-in @playwright/test | Playwright |
| Auto-wait | Limited | Full | Playwright |
| Selector engine | CSS, XPath | CSS, XPath, text, role, locators | Playwright |
| Trace viewer | No | Yes (best in class) | Playwright |
| Codegen | No | Yes (playwright codegen) | Playwright |
| Network interception | Yes | Yes | Tie |
| PDF rendering | Yes | Yes (Chromium only) | Tie |
| Bundle size | Smaller | Larger | Puppeteer |
| Ramp-up time | Faster | Slower | Puppeteer |
| Backed by | Google Chrome team | Microsoft | Tie |
Pick Playwright if you write end-to-end tests, need Safari coverage, share code with Python or .NET teams, or want a trace viewer for flaky CI runs.
Pick Puppeteer if you scrape Chrome-only sites, build PDF or screenshot tools, ship a small Node binary, or already have a Jest setup you do not want to touch.
A short history of both libraries
Puppeteer launched in 2017. Google's Chrome team built it on top of the Chrome DevTools Protocol so Node devs could drive a real Chrome from JavaScript. For three years it was the go-to for headless work. Every screenshot service and scraping how-to used it.
Playwright launched in 2020. The team that built Puppeteer left Google and joined Microsoft. They wrote a new lib from scratch with the lessons learned: cross-browser from day one, one API for Chrome, Firefox, and WebKit, and a test runner baked in. The API looks like Puppeteer on purpose, which makes a port easy.
Today both are healthy. Puppeteer ships about once a month and tracks Chrome's release pace. Playwright pushes faster — about every three weeks — and has passed Puppeteer on weekly npm downloads.
Browser support: the biggest gap
This is where the two libraries diverge most.
Puppeteer drives Chrome and Chromium by default. Firefox support exists, but the docs still flag it as a beta and it ships as a separate build. Safari is not on the menu.
Playwright drives Chrome, Firefox, and WebKit (Safari's engine) as first-class targets. The same API works in all three. WebKit on Linux is a special build the Playwright team keeps up to date, so you can run Safari-like tests inside a Linux CI box — something Safari itself cannot do.
| Browser | Puppeteer | Playwright |
|---|---|---|
| Chromium / Chrome | Yes (primary) | Yes |
| Microsoft Edge | Yes (via Chromium) | Yes |
| Firefox | Experimental | Yes (stable) |
| WebKit / Safari | No | Yes |
| Mobile Safari | No | Yes (emulation) |
| Mobile Chrome | Emulation only | Yes (emulation) |
If your product ships to the public web, Safari coverage matters. About 18 percent of desktop users and roughly half of all US mobile users browse with Safari. WebKit handles CSS, fonts, and form fields a bit different from Chrome. Playwright's WebKit catches those bugs before users do.
If you build a Chrome-only internal tool, this gap won't bite you.
API differences that show up in real code
The Puppeteer and Playwright APIs look alike at first glance — same browser.newPage(), same page.goto(). Below the surface they handle waits, selectors, and state very differently.
Auto-wait
In Puppeteer you wait for things explicitly:
// Puppeteer
await page.waitForSelector('#submit');
await page.click('#submit');
await page.waitForNavigation();In Playwright the wait is built into every action:
// Playwright
await page.click('#submit'); // auto-waits for visible + enabled
await page.waitForURL('**/dashboard');Playwright's actions wait for the element to be present, visible, stable, and enabled before they fire. This kills a whole class of flaky tests where a button is in the DOM but not yet ready to click.
Locators vs element handles
Puppeteer returns element handles. They go stale if the DOM re-renders, so you have to re-query.
// Puppeteer
const btn = await page.$('#submit');
await btn.click(); // can fail after re-renderPlaywright introduced locators, which are lazy and re-resolve on every action.
// Playwright
const btn = page.locator('#submit');
await btn.click(); // resolves at click time, survives re-rendersFor React, Vue, or Svelte apps that re-render on every state change, locators kill a steady source of flakes.
Selector strategies
Puppeteer takes CSS and XPath. Playwright adds text, role, and test-id locators that match the way real users find UI:
// Playwright — find by what users see
await page.getByRole('button', { name: 'Submit' }).click();
await page.getByLabel('Email address').fill('me@example.com');
await page.getByText('Welcome back').waitFor();These selectors stay valid when class names change in a CSS refactor. They also push you toward better markup — if Playwright cannot find the button by role, screen readers cannot either.
Network interception
Both libs can stub or block network calls. The APIs are close.
// Puppeteer
await page.setRequestInterception(true);
page.on('request', (req) => {
if (req.resourceType() === 'image') req.abort();
else req.continue();
});// Playwright
await page.route('**/*.{png,jpg,jpeg,webp}', (route) => route.abort());Playwright's page.route() takes glob patterns, which is a small ergonomic win. Both let you mock JSON, fake offline, or throttle bandwidth.
Tracing and debugging
Playwright's trace viewer is the feature most devs fall in love with. After a failed test, the viewer shows a timeline of every action, a DOM snapshot at each step, network calls, console logs, and a shot before and after each click. You can scrub a flaky test like a video.
// Playwright — record a trace on retry
test.use({ trace: 'on-first-retry' });Puppeteer has no match for it. You log, you snap on failure, you guess. For debugging flaky CI runs, this is a huge gap in practice.
Code examples: the same five tasks in both libraries
Here are five common jobs side by side. The shape of the code is close, which is why a port tends to be easy.
1. Take a full-page screenshot
// Puppeteer
import puppeteer from 'puppeteer';
const browser = await puppeteer.launch();
const page = await browser.newPage();
await page.goto('https://example.com');
await page.screenshot({ path: 'page.png', fullPage: true });
await browser.close();// Playwright
import { chromium } from 'playwright';
const browser = await chromium.launch();
const page = await browser.newPage();
await page.goto('https://example.com');
await page.screenshot({ path: 'page.png', fullPage: true });
await browser.close();If your goal is comparing UI shots across builds in CI, our visual regression testing guide walks through tools that build on top of both libs.
2. Fill a login form
// Puppeteer
await page.type('#email', 'user@example.com');
await page.type('#password', 'secret');
await page.click('#login');
await page.waitForNavigation();// Playwright
await page.getByLabel('Email').fill('user@example.com');
await page.getByLabel('Password').fill('secret');
await page.getByRole('button', { name: 'Log in' }).click();
await page.waitForURL('**/dashboard');3. Click a button and wait for content
// Puppeteer
await page.click('#load-more');
await page.waitForSelector('.item:nth-child(20)');// Playwright
await page.getByRole('button', { name: 'Load more' }).click();
await expect(page.locator('.item')).toHaveCount(20);4. Wait for a selector to appear
// Puppeteer
await page.waitForSelector('.toast-success', { timeout: 5000 });// Playwright
await expect(page.locator('.toast-success')).toBeVisible({ timeout: 5000 });5. Intercept and mock a network call
// Puppeteer
await page.setRequestInterception(true);
page.on('request', (req) => {
if (req.url().includes('/api/users')) {
req.respond({
status: 200,
contentType: 'application/json',
body: JSON.stringify([{ id: 1, name: 'Test' }]),
});
} else {
req.continue();
}
});// Playwright
await page.route('**/api/users', (route) =>
route.fulfill({
status: 200,
contentType: 'application/json',
body: JSON.stringify([{ id: 1, name: 'Test' }]),
})
);Test framework integration
This is the silent decision driver for most teams.
Puppeteer ships no test runner. You bring Jest, Mocha, or Vitest. You write your own helpers for fixtures, retries, parallel runs, and HTML reports. The community ships jest-puppeteer and puppeteer-cluster, but they are third-party and patchy.
Playwright ships @playwright/test in the same install. You get:
- A test runner with sharded parallel runs
- Fixtures for shared state (browser, page, auth)
- Auto-retry on failure
- HTML report with shots, videos, and traces
- Built-in
expect()with web-aware checks (toBeVisible,toHaveURL,toContainText) - Codegen — record a test by clicking through your app
If end-to-end testing is the goal, the runner gap alone is enough to pick Playwright. You skip a week of setup work.
If your goal is scraping or PDF jobs, you do not need a runner. Puppeteer is the cleaner fit.
Performance benchmarks
Both libs are fast. They are also roughly tied — when you stack like to like, the gap is small.
Public benchmarks (Skyvern 2025, indie dev blogs) show:
- Cold start: Puppeteer launches Chrome about 5–10 percent faster on a clean machine. Playwright caches its browsers in its own folder and loads a bit more setup code.
- Single page load: Roughly tied. The slow part is the network and the page itself, not the library.
- Parallel runs: Playwright's built-in runner shards across workers cleanly. Puppeteer needs
puppeteer-clusteror your own pool, and results depend on how well you wrote it. - Memory per browser context: Roughly tied. Both reuse contexts well if you do.
Real-world gaps come from your code, not the library. A pool of 50 reused Puppeteer contexts beats 50 fresh Playwright launches. The flip side: Playwright's browserContext walls are built in, while Puppeteer asks you to manage them.
For a side-by-side on downloads and stars, the npm trends page updates each week. Playwright passed Puppeteer on weekly downloads in 2023 and the gap keeps growing.
Tired of plain screenshots? Try ScreenSnap Pro.
Beautiful backgrounds, pro annotations, GIF recording, and instant cloud sharing — all in one app. Pay $29 once, own it forever.
See what it doesCI/CD considerations
Both libs ship Docker images. Both support headless mode. There are real gaps in the day-to-day.
Docker images:
- Puppeteer:
ghcr.io/puppeteer/puppeteerships Chrome and anodeuser. - Playwright:
mcr.microsoft.com/playwrightships all three browsers plus system deps — about 1.5 GB. There is also a slim build.
Sharding and parallel runs: Playwright's runner shards tests across workers with one flag. Puppeteer leaves it to you.
Retries on flake: Built into Playwright. DIY in Puppeteer.
Artifacts: Playwright auto-saves traces, videos, and shots on failure. Puppeteer saves nothing by default.
Cost: Playwright's bigger image means slower pulls, but parallel sharding usually pays the time back.
If you run a screenshot service, Puppeteer in a slim Alpine image is hard to beat on cost. If you run an E2E suite of hundreds of tests, Playwright's runner saves real hours each week. For Mac devs who also need quick local captures while debugging CI, our piece on how to automate screenshots on Mac covers the desktop side.
Language support
Puppeteer is JavaScript and TypeScript only. There is a community Python port (pyppeteer) but it is no longer kept up and lags Chrome by a year or more.
Playwright ships first-party clients for:
- JavaScript / TypeScript
- Python
- Java
- .NET (C#)
The API is nearly the same across all four. A Python QA team and a Node frontend team can share test patterns, shots, and even fixtures with thin wrappers.
For multi-language shops this single fact often settles the debate.
Decision framework: when each library wins
Use this short list to pick fast.
Pick Puppeteer when:
- You only target Chrome
- You build a small scraping, screenshot, or PDF service
- You already use Jest or Mocha and don't want a second runner
- You care about a small bundle and fast cold start
- The team is JavaScript-only
Pick Playwright when:
- You write end-to-end tests against real user browsers
- You need Safari / WebKit coverage
- You want a trace viewer for flaky CI runs
- You share code across Python, Java, or .NET teams
- You want auto-wait, locators, and codegen out of the box
- Your app re-renders a lot (React, Vue, Svelte)
Use both when:
- Live scraping uses Puppeteer for speed and small deps
- The QA suite uses Playwright for cross-browser trust
- They live in different services or repos and never mix
This is more common than folks admit. The two libs solve close but not the same problems.
Migrating from Puppeteer to Playwright
The good news: the API overlap makes the port less painful than most. Most teams report a few days of work for a mid-size codebase, not weeks.
Steps:
- Install
playwrightand@playwright/testnext to Puppeteer. - Swap
puppeteer.launch()forchromium.launch(). Most calls work as-is. - Swap element handles for locators (
page.$('#x')→page.locator('#x')). - Drop the
waitForSelectorcalls beforeclick— auto-wait handles them. - Swap
setRequestInterceptionforpage.route(). - If you used Jest, port tests to
@playwright/testto unlock fixtures and traces (optional but a good idea).
Microsoft keeps a migration guide with a full map. There is also a community playwright-puppeteer shim that lets old code run on Playwright with light tweaks — handy for a staged port.
If you're starting a brand-new project in 2026 and not sure, pick Playwright. The cost of switching later is real.
Where ScreenSnap Pro fits (and where it doesn't)
If you landed here by mistake — looking for a desktop screenshot tool, not a code library — neither Puppeteer nor Playwright is what you want. They are tools you call from Node, not apps you click. For local screen capture, GIF recording, and quick markup on Mac or Windows, ScreenSnap Pro is a closer fit at $29 one-time. For server-side webpage capture, stick with this guide.
For a broader look at SaaS and self-hosted screenshot options at scale, see our roundup of the best screenshot APIs in 2026, which lines up ScreenshotOne, Urlbox, and self-hosted Puppeteer or Playwright on cost and speed.
Detailed feature matrix
A second pass with more rows for buyers who want the full view.
| Feature | Puppeteer | Playwright |
|---|---|---|
| First release | 2017 | 2020 |
| Maintainer | Google Chrome team | Microsoft |
| Languages | JS, TS | JS, TS, Python, Java, .NET |
| Chromium support | Stable | Stable |
| Firefox support | Experimental | Stable |
| WebKit support | No | Stable |
| Mobile emulation | Basic | Full (devices preset) |
| Auto-wait on actions | Limited | Yes |
| Locators (lazy) | No | Yes |
| Role / text / label selectors | No | Yes |
| Network interception | Yes | Yes |
| HAR replay | No | Yes |
| Network throttling | Yes (CDP) | Yes |
| Geolocation / permissions | Yes | Yes |
| Built-in test runner | No | Yes |
| Parallel sharding | DIY | Built-in |
| Retries on flake | DIY | Built-in |
| Trace viewer | No | Yes |
| Codegen | No | Yes |
| Screenshot to PDF | Yes (Chromium) | Yes (Chromium) |
| Multi-tab support | Yes | Yes |
| Browser contexts (isolation) | Yes | Yes |
| Headless mode | Yes | Yes |
| Headed mode | Yes | Yes |
| iframe handling | Yes | Yes (cleaner API) |
| Shadow DOM piercing | Manual | Native |
| File downloads | Yes | Yes (cleaner API) |
| File uploads | Yes | Yes |
| TypeScript types | Yes | Yes |
| Docker image | Yes | Yes (with all browsers) |
| Bundle size (npm install) | ~170 MB | ~400 MB (all browsers) |
| Weekly npm downloads (2026) | ~3.5M | ~12M+ |
Frequently Asked Questions
Final verdict
For end-to-end testing in 2026, Playwright is the default. The runner, locators, trace viewer, and cross-browser cover stack up into hours saved every week.
For Chrome-only scraping, screenshots, or PDF jobs, Puppeteer still earns its place. Smaller install, simpler model, less to learn.
If you're building both — a public app you test plus a scraping pipe — running the right tool for each job is fine. Code sharing matters less than picking the tool that fits the workload.
Whichever you ship, lean on the trace viewer (Playwright) or solid logs plus failure shots (Puppeteer). Browser automation is flaky by nature. Good debugging is the gap between a calm CI and a Slack feed of red alerts. And when a real bug slips through, a clean bug report template helps your team triage faster.
Morgan
Indie DeveloperIndie developer, founder of ScreenSnap Pro. A decade of shipping consumer Mac apps and developer tools. Read full bio
@m_0_r_g_a_n_