← Back to Blog

Puppeteer vs Playwright 2026: Compared

By MorganPublished April 30, 202617 min read

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.

DimensionPuppeteerPlaywrightWinner
Browser supportChrome, Firefox (beta)Chromium, Firefox, WebKitPlaywright
LanguagesJS, TS onlyJS, TS, Python, Java, .NETPlaywright
Test runnerNone (BYO Jest/Mocha)Built-in @playwright/testPlaywright
Auto-waitLimitedFullPlaywright
Selector engineCSS, XPathCSS, XPath, text, role, locatorsPlaywright
Trace viewerNoYes (best in class)Playwright
CodegenNoYes (playwright codegen)Playwright
Network interceptionYesYesTie
PDF renderingYesYes (Chromium only)Tie
Bundle sizeSmallerLargerPuppeteer
Ramp-up timeFasterSlowerPuppeteer
Backed byGoogle Chrome teamMicrosoftTie

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.

Browser support matrix for Puppeteer and Playwright
Browser support matrix for Puppeteer and Playwright

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.

BrowserPuppeteerPlaywright
Chromium / ChromeYes (primary)Yes
Microsoft EdgeYes (via Chromium)Yes
FirefoxExperimentalYes (stable)
WebKit / SafariNoYes
Mobile SafariNoYes (emulation)
Mobile ChromeEmulation onlyYes (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-render

Playwright 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-renders

For 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.

Browser automation code and CI workflow diagram
Browser automation code and CI workflow diagram

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-cluster or 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.

ScreenSnap Pro
Sponsored by the makers

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 does

CI/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/puppeteer ships Chrome and a node user.
  • Playwright: mcr.microsoft.com/playwright ships 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

Decision flowchart for picking Puppeteer or Playwright
Decision flowchart for picking Puppeteer or Playwright

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:

  1. Install playwright and @playwright/test next to Puppeteer.
  2. Swap puppeteer.launch() for chromium.launch(). Most calls work as-is.
  3. Swap element handles for locators (page.$('#x')page.locator('#x')).
  4. Drop the waitForSelector calls before click — auto-wait handles them.
  5. Swap setRequestInterception for page.route().
  6. If you used Jest, port tests to @playwright/test to 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.

FeaturePuppeteerPlaywright
First release20172020
MaintainerGoogle Chrome teamMicrosoft
LanguagesJS, TSJS, TS, Python, Java, .NET
Chromium supportStableStable
Firefox supportExperimentalStable
WebKit supportNoStable
Mobile emulationBasicFull (devices preset)
Auto-wait on actionsLimitedYes
Locators (lazy)NoYes
Role / text / label selectorsNoYes
Network interceptionYesYes
HAR replayNoYes
Network throttlingYes (CDP)Yes
Geolocation / permissionsYesYes
Built-in test runnerNoYes
Parallel shardingDIYBuilt-in
Retries on flakeDIYBuilt-in
Trace viewerNoYes
CodegenNoYes
Screenshot to PDFYes (Chromium)Yes (Chromium)
Multi-tab supportYesYes
Browser contexts (isolation)YesYes
Headless modeYesYes
Headed modeYesYes
iframe handlingYesYes (cleaner API)
Shadow DOM piercingManualNative
File downloadsYesYes (cleaner API)
File uploadsYesYes
TypeScript typesYesYes
Docker imageYesYes (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.

Author
Morgan

Morgan

Indie Developer

Indie developer, founder of ScreenSnap Pro. A decade of shipping consumer Mac apps and developer tools. Read full bio

@m_0_r_g_a_n_
ScreenSnap Pro — turn plain screenshots into polished visuals with backgrounds and annotations
Available formacOS&Windows

Make every screenshot look pro.

ScreenSnap Pro turns plain screenshots into polished visuals — backgrounds, annotations, GIF recording, and instant cloud links.

See ScreenSnap Pro