Playwright vs Puppeteer (2026)
TL;DR
Playwright has superseded Puppeteer for most browser automation tasks. Playwright (~7M weekly downloads) supports Chromium, Firefox, and WebKit, has a cleaner async API, and was designed from the ground up for reliability. Puppeteer (~4M downloads) remains Google's official Chrome DevTools Protocol library — still solid for Chrome-only automation, web scraping, and PDF generation. If you're starting fresh, use Playwright.
Key Takeaways
- Playwright: ~7M weekly downloads — Puppeteer: ~4M (npm, March 2026)
- Playwright supports all browsers — Puppeteer is Chromium/Chrome-focused
- Playwright was built by ex-Puppeteer team — Playwright is the "Puppeteer v2"
- Both use Chrome DevTools Protocol — Playwright adds its own protocol layer
- Puppeteer is official Google project — Better Chrome integration, first to new CDP features
Origin Story
Playwright was created by Microsoft in 2020 by the same engineers who originally built Puppeteer at Google. The project started as a fork of Puppeteer with the goal of fixing its fundamental limitations — particularly single-browser support and flaky async behavior.
Understanding this history explains the API similarities and why Playwright feels like an evolution rather than a competitor. The same team, the same CDP foundations, but a clean slate to fix the design decisions that made Puppeteer painful to use at scale.
Browser Support
This is where Playwright and Puppeteer diverge most clearly. Puppeteer supports Chromium and Chrome — full stop. Firefox support exists but is experimental and rarely used in production. Safari/WebKit is not supported at all.
Playwright supports all three major browser engines out of the box: Chromium, Firefox, and WebKit. WebKit is particularly important because it is the engine that powers Safari and iOS browsers. If your web app has Safari-specific layout bugs or JavaScript compatibility issues, Playwright is the only headless testing tool that can catch them automatically in CI.
// Playwright — launch any browser engine from the same API
import { chromium, firefox, webkit } from 'playwright';
// Run against Chrome
const chromeBrowser = await chromium.launch({ headless: true });
// Run against Firefox
const firefoxBrowser = await firefox.launch({ headless: true });
// Run against Safari/WebKit — not possible with Puppeteer at all
const safBrowser = await webkit.launch({ headless: true });
// Or configure all three in playwright.config.ts:
import { defineConfig, devices } from '@playwright/test';
export default defineConfig({
projects: [
{ name: 'chromium', use: { ...devices['Desktop Chrome'] } },
{ name: 'firefox', use: { ...devices['Desktop Firefox'] } },
{ name: 'webkit', use: { ...devices['Desktop Safari'] } },
{ name: 'mobile-safari', use: { ...devices['iPhone 14'] } },
{ name: 'mobile-chrome', use: { ...devices['Pixel 7'] } },
],
});
Running cross-browser tests in Playwright takes no additional code — you write one test, configure the project matrix, and Playwright runs it against every engine in parallel.
API and Auto-Waiting
Both Playwright and Puppeteer use async/await and share a similar API surface, reflecting their shared origin. But Playwright's auto-waiting behavior is one of the most practically significant differences for reliability.
In Puppeteer, you must explicitly wait for elements before interacting with them. Forgetting a waitForSelector() call is one of the most common sources of flaky tests. Playwright auto-waits: before any interaction, it waits for the element to be attached to the DOM, visible, stable (not animating), and enabled (not disabled).
// Puppeteer — manual waiting required
const page = await browser.newPage();
await page.goto('https://app.example.com/checkout');
// Must wait before interacting — easy to forget, easy to get wrong
await page.waitForSelector('#place-order-button');
await page.click('#place-order-button');
// Also common: wait for navigation after click
await Promise.all([
page.waitForNavigation(),
page.click('#place-order-button'),
]);
// Playwright — auto-waiting built in
const page = await context.newPage();
await page.goto('https://app.example.com/checkout');
// No explicit wait needed — Playwright waits automatically for:
// - Element to be in DOM
// - Element to be visible
// - Element to be stable (not animating)
// - Element to be enabled
await page.click('#place-order-button');
// Navigation is awaited automatically when it results from an action
The auto-waiting behavior dramatically reduces boilerplate and makes tests less flaky. You can still add explicit waits when needed, but they are the exception rather than the rule.
API Comparison
// Puppeteer — direct CDP, Chrome-first
import puppeteer from 'puppeteer';
const browser = await puppeteer.launch({ headless: 'new' });
const page = await browser.newPage();
await page.goto('https://example.com');
await page.type('#search', 'puppeteer vs playwright');
await page.click('button[type="submit"]');
await page.waitForSelector('.results');
const results = await page.$$eval('.result-title', els =>
els.map(el => el.textContent)
);
await browser.close();
// Playwright — multi-browser, built for reliability
import { chromium } from 'playwright';
const browser = await chromium.launch({ headless: true });
const context = await browser.newContext();
const page = await context.newPage();
await page.goto('https://example.com');
await page.fill('#search', 'puppeteer vs playwright');
await page.click('button[type="submit"]');
await page.waitForSelector('.results');
const results = await page.$$eval('.result-title', els =>
els.map(el => el.textContent)
);
await browser.close();
The APIs are intentionally similar. Playwright adds a BrowserContext layer between browser and page — enabling multiple isolated sessions per browser instance.
Browser Context: Playwright's Key Advantage
// Playwright — multiple isolated users in one browser
const browser = await chromium.launch();
// Create two isolated contexts (like separate browser profiles)
const userAContext = await browser.newContext({
storageState: 'user-a-auth.json', // Pre-authenticated
});
const userBContext = await browser.newContext({
storageState: 'user-b-auth.json',
});
const pageA = await userAContext.newPage();
const pageB = await userBContext.newPage();
// Run in parallel with full isolation — cookies, localStorage, etc.
await Promise.all([
pageA.goto('https://app.example.com/dashboard'),
pageB.goto('https://app.example.com/dashboard'),
]);
// Puppeteer — no built-in context isolation
// Must launch separate browser instances for true isolation
const browserA = await puppeteer.launch();
const browserB = await puppeteer.launch();
// Doubles memory usage and startup time
For testing flows that involve multiple user roles or concurrent sessions, Playwright's context model is significantly better.
Test Infrastructure
This is the biggest structural difference between the two tools. Puppeteer is a browser automation API — it handles controlling the browser and nothing else. To write tests, you add Jest, Mocha, or another test runner separately. You manage parallelism, assertions, and reporting yourself.
Playwright ships @playwright/test as a first-class test runner. This includes built-in test runner, parallelism, fixtures, rich assertions, trace viewer, HTML reporter, screenshot comparison, and video recording on failure.
// @playwright/test — a complete test suite, no additional setup
import { test, expect } from '@playwright/test';
test.describe('Checkout flow', () => {
test.beforeEach(async ({ page }) => {
await page.goto('/');
await page.getByRole('link', { name: 'Shop' }).click();
});
test('adds item to cart', async ({ page }) => {
await page.getByRole('button', { name: 'Add to Cart' }).first().click();
await expect(page.getByTestId('cart-count')).toHaveText('1');
});
test('completes checkout', async ({ page }) => {
// Built-in assertions wait automatically
await expect(page.getByRole('heading', { name: 'Order Confirmed' }))
.toBeVisible({ timeout: 10000 });
});
});
# Run tests in parallel across 4 workers
npx playwright test --workers 4
# Generate HTML report
npx playwright test --reporter html
# Show trace for failed tests
npx playwright show-trace trace.zip
The trace viewer is especially powerful — when a test fails in CI, you get a full timeline of every action, network request, and DOM snapshot, making it possible to debug CI failures without re-running locally.
Web Scraping
Both tools are heavily used for web scraping. The API choice largely comes down to preference, but there are real ecosystem differences.
Puppeteer has a larger and more established scraping ecosystem. Apify (one of the largest web scraping platforms) built its flagship SDK around Puppeteer before adding Playwright support. Crawlee, their open-source scraping library, supports both but has more Puppeteer examples and community recipes.
Playwright's context.route() for request interception is architecturally cleaner than Puppeteer's page-level setRequestInterception, and Playwright's cross-browser support means you can scrape sites that behave differently across browsers. Playwright stealth mode (via community plugins) is increasingly preferred for scraping sites with bot detection.
// Puppeteer web scraping with request filtering
const page = await browser.newPage();
await page.setRequestInterception(true);
page.on('request', (req) => {
if (['image', 'stylesheet', 'font'].includes(req.resourceType())) {
req.abort();
} else {
req.continue();
}
});
await page.goto('https://news.ycombinator.com');
const stories = await page.$$eval('.athing', els =>
els.map(el => ({
title: el.querySelector('.titleline a')?.textContent,
url: el.querySelector('.titleline a')?.href,
}))
);
// Playwright equivalent — context-level routing
const context = await browser.newContext();
await context.route('**/*.{png,jpg,css,woff}', route => route.abort());
const page = await context.newPage();
await page.goto('https://news.ycombinator.com');
const stories = await page.$$eval('.athing', els =>
els.map(el => ({
title: el.querySelector('.titleline a')?.textContent,
url: el.querySelector('.titleline a')?.href,
}))
);
Playwright's context.route() applies interception rules to all pages in the context at once, which is cleaner for multi-page scrapers.
Where Puppeteer Still Wins
Official Chrome DevTools Protocol
Puppeteer is Google's reference implementation for CDP. It gets new Chrome features first, official debugging support, and Chrome-specific APIs that Playwright doesn't expose.
// Puppeteer — access Chrome-specific CDP features
const client = await page.target().createCDPSession();
// Chrome Performance API
await client.send('Performance.enable');
const metrics = await client.send('Performance.getMetrics');
// Network emulation
await client.send('Network.emulateNetworkConditions', {
offline: false,
downloadThroughput: 1.5 * 1024 * 1024 / 8, // 1.5 Mbps
uploadThroughput: 750 * 1024 / 8,
latency: 40,
});
PDF Generation
// Puppeteer — PDF generation is a first-class feature
const page = await browser.newPage();
await page.goto('https://example.com/report', { waitUntil: 'networkidle2' });
await page.pdf({
path: 'report.pdf',
format: 'A4',
margin: { top: '20px', right: '20px', bottom: '20px', left: '20px' },
printBackground: true,
displayHeaderFooter: true,
headerTemplate: '<span class="date"></span>',
footerTemplate: '<span class="pageNumber"></span> of <span class="totalPages"></span>',
});
Playwright also supports PDF generation, but Puppeteer's implementation is more mature and battle-tested.
Smaller Dependency for Chrome-Only Tasks
Puppeteer package size: ~3MB (without browser)
@puppeteer/browsers: Manages Chrome binary downloads separately
Playwright package: Downloads browsers for all platforms (~300MB+)
Can use --with-deps=chromium to limit to one browser
Debugging and Developer Experience
One area where Playwright has invested heavily is developer tooling. The Playwright test runner includes several features that have no Puppeteer equivalent: a trace viewer, test report HTML dashboard, screenshot diffing for visual regression, and a UI mode for interactive test development.
The trace viewer is the most useful in practice. When a test fails in CI, Playwright captures a .zip file containing a full timeline: every page.goto, every page.click, the network requests made, console errors, and DOM snapshots at each step. You can open this locally and scrub through the exact sequence of events that led to the failure. Debugging flaky CI tests without re-running them locally is a significant workflow improvement.
# Generate traces for all failing tests
npx playwright test --trace on
# View a trace from a failed CI run
npx playwright show-trace test-results/my-test/trace.zip
Puppeteer has no equivalent tooling built in. Debugging typically means adding await page.screenshot() calls, console.log statements, or running the browser in non-headless mode with headless: false. These are functional but manual approaches that don't scale to large test suites.
Playwright also ships playwright codegen — a browser automation recorder that watches you interact with a page and generates the corresponding Playwright code. This is useful when writing tests for complex multi-step flows and dramatically reduces the time to produce an initial test skeleton.
Language Bindings and Ecosystem
Playwright's multi-language support is a significant practical advantage in organizations with polyglot teams or non-JavaScript backends. The same Playwright test API is available in Python, Java, and C# in addition to Node.js. A QA team that writes Python can use Playwright without adopting Node.js tooling.
Puppeteer is Node.js only. This is not a limitation for most JavaScript-first projects, but it is a real constraint for teams with Java or Python backends who want to share a testing framework across services.
Both tools have active communities and comprehensive documentation. Playwright's documentation is notably thorough, with guides on authentication, network mocking, API testing (Playwright's HTTP request API can test REST endpoints without a browser), and mobile emulation.
Package Health
| Package | Weekly Downloads | Maintainer | Language Bindings |
|---|---|---|---|
playwright | ~7M | Microsoft team | Node.js, Python, Java, C# |
puppeteer | ~4M | Google Chrome team | Node.js |
Playwright is maintained by a dedicated Microsoft team and has seen aggressive development since 2020. Language bindings for Python, Java, and C# make Playwright relevant beyond the JavaScript ecosystem. Puppeteer is maintained by the Google Chrome team with a focus on the Chrome DevTools Protocol — it tends to get new Chrome features before Playwright does.
Both are healthy, actively maintained projects. Neither is at risk of abandonment. The trajectory since 2021 shows Playwright gaining ground at roughly 1-2M downloads per quarter.
When to Choose
Choose Playwright when:
- Testing across multiple browsers (especially Safari/iOS)
- Using Playwright Test runner (parallel execution, fixtures, trace viewer, reporting)
- You need browser context isolation for multi-user scenarios
- Building new automation from scratch
- TypeScript is important (Playwright has excellent TS support and multi-language bindings)
- E2E testing is the primary goal
Choose Puppeteer when:
- You need Chrome-specific CDP APIs
- PDF generation is the primary use case
- You're already using Puppeteer and it works well
- You want the smallest possible Chrome-only dependency
- Chrome DevTools integration is required
- Web scraping with Apify/Crawlee, where Puppeteer has more established ecosystem support
For more on E2E testing tooling choices and detailed configuration walkthroughs, see the Playwright vs Puppeteer comparison on PkgPulse. If you're setting up Playwright from scratch, the E2E test setup guide covers project configuration, fixtures, and CI integration. You can also track download trends and health scores directly on the Playwright package page.
See the live comparison
View playwright vs. puppeteer on PkgPulse →