Playwright and Puppeteer share DNA. The core Puppeteer team left Google for Microsoft in 2020 and built Playwright to fix the limitations they couldn't address inside Google's ecosystem. Six years later, the tools serve different purposes — and choosing between them affects your team's velocity more than most architecture decisions.
This article covers every dimension that matters: browser support, auto-waiting, locator strategy, test runners, API testing, parallelism, AI integration, raw performance, and a concrete migration guide. If you're evaluating Playwright vs Puppeteer in 2026, this is the only comparison you need.
Quick Verdict
Playwright wins for testing. It has auto-waiting, a built-in test runner, multi-browser coverage, API testing support, worker-based parallelism, trace viewer debugging, and an AI integration layer (MCP Server) that Puppeteer simply doesn't have. If you're building or maintaining a test suite, Playwright is the clear choice in 2026.
Puppeteer still has a place — but it's narrower than it used to be. If you need a lightweight Chromium-only automation library for web scraping, PDF generation, or quick scripting tasks where you don't need cross-browser coverage or a test runner, Puppeteer's smaller footprint and simpler API make it a reasonable pick.
TL;DR: Building a test suite? Use Playwright. Writing a one-off scraping script that only needs Chrome? Puppeteer is fine. For everything else, Playwright's richer API and ecosystem make it the safer long-term bet.
Origins & Maintainers
Understanding where each tool came from explains why they differ today.
Puppeteer was released by Google in January 2018 as a Node.js library for controlling headless Chrome via the DevTools Protocol. It quickly became the de facto standard for browser automation, replacing older tools like PhantomJS and Nightmare. Google maintains it as an open-source project under the Chrome DevTools team.
Playwright launched in January 2020 when the core Puppeteer engineers — Andrey Lushnikov, Pavel Feldman, and others — moved to Microsoft. They rebuilt the automation layer from scratch with first-class multi-browser support, auto-waiting, and a built-in test runner. Microsoft maintains it actively, with weekly releases and a dedicated team.
The key takeaway: Playwright isn't a fork of Puppeteer. It's a ground-up rewrite by the same people who knew exactly what Puppeteer got wrong. Every design decision in Playwright — auto-waiting, browser contexts, the locator API — directly addresses a pain point the team experienced while building and supporting Puppeteer at Google. (For how Playwright compares to other frameworks, see our Playwright vs Selenium and Playwright vs Cypress comparisons.)
Browser Support
This is the most visible difference between the two tools and the one that affects real-world test coverage the most.
Puppeteer: Chromium + Experimental Firefox
Puppeteer officially supports Chromium (and by extension, Chrome and Edge). It added experimental Firefox support in 2023, but the implementation remains incomplete — many APIs behave differently or throw unsupported errors in Firefox mode. There is no WebKit (Safari) support at all.
import puppeteer from 'puppeteer'; // Chromium only (default) const browser = await puppeteer.launch(); // Firefox (experimental, limited API support) const firefox = await puppeteer.launch({ product: 'firefox' });
Playwright: Chromium + Firefox + WebKit
Playwright supports all three browser engines with full API parity. You write your test once and run it across Chromium, Firefox, and WebKit (Safari's rendering engine). This means you catch Safari-specific CSS bugs, Firefox event handling quirks, and Chromium-only regressions — all in the same CI pipeline.
// playwright.config.ts export default defineConfig({ projects: [ { name: 'chromium', use: { ...devices['Desktop Chrome'] } }, { name: 'firefox', use: { ...devices['Desktop Firefox'] } }, { name: 'webkit', use: { ...devices['Desktop Safari'] } }, { name: 'mobile', use: { ...devices['iPhone 15'] } }, ], });
Why this matters: Safari accounts for ~18% of global web traffic. If you're only testing on Chromium, you're shipping untested code to nearly 1 in 5 users. Playwright's WebKit support closes that gap without needing a separate tool or a Mac-only CI runner.
Auto-Waiting
Auto-waiting is the single biggest quality-of-life difference between the two tools. It's the reason Playwright tests are less flaky out of the box.
Puppeteer: Manual Waiting Required
In Puppeteer, clicking an element that hasn't rendered yet throws an error. You must manually wait for elements before interacting with them. This leads to either waitForSelector calls before every action (verbose) or waitForTimeout calls (flaky).
// Must wait manually before every interaction await page.waitForSelector('#submit-btn'); await page.click('#submit-btn'); // Or worse: hardcoded waits await page.waitForTimeout(2000); // fragile! await page.click('#submit-btn');
Playwright: Built-In Auto-Waiting
Playwright's locator actions — click(), fill(), check(), etc. — automatically wait for the element to be visible, enabled, stable (not animating), and ready to receive events. You never write a waitForSelector call. Ever.
// Auto-waits for visibility, stability, enabled state await page.getByRole('button', { name: 'Submit' }).click(); // Web-first assertions also auto-retry await expect(page.getByText('Success')).toBeVisible();
await page.getByRole('button').click()await page.getByLabel('Email').fill('a@b.com')No
waitForSelector needed
await page.waitForSelector('#btn')await page.click('#btn')Two lines for every interaction
This difference compounds across a test suite. A 500-test suite in Puppeteer might have 2,000+ manual waitForSelector calls. In Playwright, that number is zero.
Locator Strategy
How you find elements on the page determines how resilient your tests are to UI changes.
Puppeteer: CSS Selectors and XPath
Puppeteer uses CSS selectors and XPath as its primary element-finding strategy. These are tightly coupled to the DOM structure, which means they break when developers refactor HTML, rename classes, or restructure components.
// Fragile: tied to CSS class names and DOM structure await page.click('.btn-primary.submit-form'); await page.type('#email-input', 'user@test.com'); // XPath: even more brittle const [el] = await page.$x('//div[@class="form"]/button[1]'); await el.click();
Playwright: Semantic Locators (getByRole, getByLabel, getByText)
Playwright encourages role-based locators that target what an element is rather than where it sits in the DOM. These locators survive UI redesigns because they're based on ARIA semantics, visible text, and accessible labels — things that shouldn't change when you refactor CSS or restructure HTML.
// Resilient: targets the element's purpose await page.getByRole('button', { name: 'Submit' }).click(); await page.getByLabel('Email address').fill('user@test.com'); // Chaining for scoped lookups await page.getByRole('dialog') .getByRole('button', { name: 'Confirm' }) .click();
Bonus: Playwright's locator strategy doubles as an accessibility audit. If getByRole('button', { name: 'Submit' }) can't find your submit button, that button likely has accessibility issues that affect screen reader users.
Test Runner
A test runner handles test discovery, execution, reporting, parallelism, retries, and fixtures. It's the backbone of any test suite.
Puppeteer: No Built-In Test Runner
Puppeteer is a browser automation library, not a testing framework. To write tests, you need to pair it with a separate test runner like Jest, Mocha, or Vitest. This means configuring:
- Test runner setup and teardown hooks
- Browser lifecycle management (launch, close, error handling)
- Parallel execution configuration
- Reporter plugins for HTML/JSON output
- Retry logic for flaky tests
- Screenshot/artifact capture on failure
// jest.setup.js const puppeteer = require('puppeteer'); let browser; beforeAll(async () => { browser = await puppeteer.launch(); }); afterAll(async () => { await browser.close(); }); beforeEach(async () => { global.page = await browser.newPage(); }); afterEach(async () => { await global.page.close(); });
Playwright Test: Everything Built In
Playwright Test (@playwright/test) is a full-featured test runner designed specifically for browser testing. Parallelism, retries, fixtures, HTML reporter, trace viewer, screenshot capture, and video recording are all built in and configured through a single playwright.config.ts file.
import { test, expect } from '@playwright/test'; test('user can log in', async ({ page }) => { await page.goto('https://app.example.com/login'); await page.getByLabel('Email').fill('user@test.com'); await page.getByLabel('Password').fill('secret'); await page.getByRole('button', { name: 'Sign in' }).click(); await expect(page).toHaveURL('/dashboard'); }); // No setup, no teardown, no browser management // Playwright Test handles everything
Playwright Test also ships with codegen — a test recorder that watches you interact with a page and generates test code automatically. Puppeteer has no equivalent.
API Testing
Modern testing requires validating both the UI and the API layer. Playwright lets you do both in the same framework.
Playwright: Built-In request Context
Playwright includes APIRequestContext for making HTTP requests without a browser. You can seed test data, validate API responses, and combine API + UI assertions in a single test — all without installing Axios, Supertest, or any HTTP client.
import { test, expect } from '@playwright/test'; test('API returns user data', async ({ request }) => { const response = await request.get('/api/users/1'); await expect(response).toBeOK(); const data = await response.json(); expect(data.name).toBe('Asim Noaman'); }); test('seed data via API, verify in UI', async ({ request, page }) => { // Create user via API (fast) await request.post('/api/users', { data: { name: 'Test User', email: 'test@example.com' } }); // Verify in UI await page.goto('/admin/users'); await expect(page.getByText('Test User')).toBeVisible(); });
Puppeteer: No API Testing Support
Puppeteer has no HTTP client. To test APIs alongside browser automation, you need to install and configure a separate library like Axios, node-fetch, or Supertest. This adds dependencies, requires separate assertion patterns, and makes test data seeding more verbose.
Parallel Execution
Running tests in parallel is the fastest way to cut CI pipeline time. The two tools approach this very differently.
Playwright: Worker-Based Parallelism
Playwright Test runs tests in parallel across isolated worker processes. Each worker gets its own browser instance, eliminating shared state. Configuration is a single line in playwright.config.ts:
export default defineConfig({ fullyParallel: true, workers: '50%', // or a number like 4 // Shard across CI machines // npx playwright test --shard=1/4 });
For large suites, Playwright supports sharding — splitting the suite across multiple CI machines. A 500-test suite sharded across 4 machines with 4 workers each runs 16 tests simultaneously.
Puppeteer: Manual Parallelism
Puppeteer has no built-in parallel execution. You rely on your test runner's parallelism (Jest's --workers, Vitest's threads), and you must manually manage browser instance lifecycle, ensure test isolation, and handle resource contention.
// jest.config.js module.exports = { maxWorkers: '50%', // Each test file must manage its own browser // No shared browser instance across workers // No built-in sharding // No automatic artifact collection per worker };
The practical difference: setting up parallel execution in Playwright takes 2 lines of config. In Puppeteer, it takes a custom harness, careful lifecycle management, and often a week of debugging shared-state failures.
AI Integration
AI-assisted testing is the biggest shift in QA automation since the move from Selenium to modern frameworks. In 2026, this is a decisive differentiator.
Playwright: MCP Server + Claude AI
Playwright has an official MCP (Model Context Protocol) Server that connects directly to AI assistants like Claude. This enables:
- AI-generated tests: Describe what you want to test in plain English; Claude writes the Playwright test code
- Self-healing locators: When a locator breaks, the AI analyzes the page and suggests the correct replacement
- Visual regression analysis: AI reviews screenshot diffs and determines if changes are intentional or bugs
- Test maintenance: AI reads your existing test suite, understands patterns, and generates new tests that match your conventions
- Live browser interaction: Claude can launch a browser, navigate pages, and verify behavior in real time through MCP
// Claude AI generates this from: "test the login flow" import { test, expect } from '@playwright/test'; test('successful login redirects to dashboard', async ({ page }) => { await page.goto('/login'); await page.getByLabel('Email').fill('admin@company.com'); await page.getByLabel('Password').fill('secure-pass'); await page.getByRole('button', { name: 'Sign in' }).click(); await expect(page).toHaveURL('/dashboard'); await expect(page.getByRole('heading', { name: 'Welcome' })) .toBeVisible(); });
Puppeteer: No AI Integration Layer
Puppeteer has no MCP server, no official AI integration, and no equivalent protocol for connecting to AI assistants. You can still use AI tools to write Puppeteer code, but there's no live connection between the AI and the browser — no self-healing, no real-time page analysis, no automated test generation from a running application.
This gap will widen. Microsoft is actively investing in Playwright's AI ecosystem. Google has not signaled any plans to add similar capabilities to Puppeteer. If AI-assisted testing matters to your team (and it should), Playwright is the only choice with a roadmap.
Performance Comparison
Raw speed matters, but it's not the whole story. Here's how the two tools compare across different dimensions.
Single-Script Speed
For a single automation script (launch browser, navigate, perform actions, close), Puppeteer is ~5–10% faster than Playwright. This is because Puppeteer has less abstraction overhead — it speaks directly to Chrome DevTools Protocol without the cross-browser compatibility layer that Playwright maintains.
Test Suite Speed
For an actual test suite, Playwright is 3–5x faster than Puppeteer + Jest. The built-in worker parallelism, browser context reuse, and sharding support mean that Playwright's overhead per-test is dwarfed by its execution efficiency at scale.
Resource Usage
- Package size: Puppeteer ~180MB (downloads Chromium); Playwright ~250MB (downloads Chromium + Firefox + WebKit)
- Memory per browser context: Roughly equivalent (~50–80MB per context in both tools)
- CI execution time (200 tests): Playwright ~2.5 min (4 workers, sharded); Puppeteer + Jest ~8–12 min (manual parallelism)
Tip: If you only need Chromium for Playwright, install it with npx playwright install chromium to reduce the download to ~130MB — smaller than Puppeteer's default install.
Feature-by-Feature Comparison
| Feature | Playwright | Puppeteer |
|---|---|---|
| Browser engines | Chromium + Firefox + WebKit | Chromium + Firefox (experimental) |
| Auto-waiting | Built-in for all actions | None — manual waitForSelector |
| Locator strategy | getByRole, getByLabel, getByText | CSS selectors, XPath |
| Test runner | Playwright Test (built-in) | Requires Jest / Mocha / Vitest |
| API testing | Built-in request context | Not supported |
| Parallel execution | Worker-based + sharding | Manual via test runner |
| AI integration | MCP Server + Claude AI | None |
| Test recorder | codegen (built-in) | Chrome DevTools Recorder (separate) |
| Trace viewer | Built-in with timeline, DOM snapshots | None |
| Mobile emulation | Device presets + real WebKit | Chrome DevTools emulation only |
| Language support | JS, TS, Python, Java, C# | JS, TS only |
| Network interception | page.route() with glob patterns | page.setRequestInterception() |
| Fixtures system | Built-in test fixtures | None |
| Single-script speed | ~5–10% slower (more abstraction) | Slightly faster for Chromium-only |
| Package size | ~250MB (3 engines) | ~180MB (Chromium only) |
| Maintainer | Microsoft |
Score: Playwright 13, Puppeteer 2, Tie 4. Puppeteer's only advantages are marginal single-script speed and a smaller package size — neither of which matters for real-world testing.
Migration Guide: Puppeteer to Playwright in 5 Steps
If you have an existing Puppeteer test suite, here's a concrete migration path. The APIs share common ancestry, so most changes are mechanical.
Install Playwright Test and its browsers. Remove Puppeteer and its Jest/Mocha config.
# Install Playwright npm init playwright@latest # Remove Puppeteer + test runner boilerplate npm uninstall puppeteer jest @types/jest jest-puppeteer
Delete all beforeAll/afterAll browser launch/close code and beforeEach/afterEach page creation. Playwright Test's fixture system handles this automatically via the { page } parameter.
test('...', async ({ page }) => { // page is ready, no setup needed});
beforeAll: launch browserbeforeEach: create pageafterEach: close pageafterAll: close browser
This is the biggest change. Replace page.$('css') and page.click('css') with semantic locators. Use this mapping:
// Puppeteer // Playwright page.click('button.submit') => page.getByRole('button', { name: 'Submit' }).click() page.type('#email', '...') => page.getByLabel('Email').fill('...') page.$('.error-msg') => page.getByText('Error message') page.waitForSelector('.done') => DELETE (auto-waiting handles it) page.waitForTimeout(2000) => DELETE (never needed)
Swap Jest's synchronous expect() for Playwright's async await expect() assertions that auto-retry. This eliminates race conditions.
// Puppeteer + Jest (before) const text = await page.$eval('.title', el => el.textContent); expect(text).toBe('Dashboard'); // Playwright (after) — auto-retries until true or timeout await expect(page.getByRole('heading', { name: 'Dashboard' })) .toBeVisible();
Set up your projects (browsers), base URL, retries, and reporter. Then run with npx playwright test.
import { defineConfig, devices } from '@playwright/test'; export default defineConfig({ testDir: './tests', fullyParallel: true, retries: 2, workers: '50%', reporter: 'html', use: { baseURL: 'https://staging.example.com', trace: 'on-first-retry', screenshot: 'only-on-failure', }, projects: [ { name: 'chromium', use: { ...devices['Desktop Chrome'] } }, { name: 'firefox', use: { ...devices['Desktop Firefox'] } }, { name: 'webkit', use: { ...devices['Desktop Safari'] } }, ], });
Timeline estimate: A 50-file Puppeteer + Jest project typically takes 1–2 days to migrate. Most of the work is Step 3 (converting selectors). Steps 1, 2, 4, and 5 take about an hour combined. If you're new to Playwright, our beginner's getting-started guide covers the fundamentals.
When to Use Each Tool
Despite Playwright's advantages in testing, there are legitimate reasons to use Puppeteer in specific scenarios. Here's a decision matrix.
Use Playwright When:
- Building an E2E or integration test suite
- You need cross-browser coverage (Chromium + Firefox + WebKit)
- You want a built-in test runner with parallelism, retries, and reporting
- Your team uses Python, Java, or C# (Puppeteer is JS/TS only)
- AI-assisted test generation is on your roadmap
- You need API + UI testing in one framework
- You want trace viewer debugging for CI failures
- You need mobile device emulation with real WebKit
Use Puppeteer When:
- Writing a one-off web scraping script that only needs Chromium
- Generating PDFs or screenshots as a microservice
- Building a Chrome extension's automated workflow
- You need the smallest possible package size for a serverless function
- Your existing codebase is deeply integrated with Puppeteer and migration cost is prohibitive
Important: "We already use Puppeteer" is not a strong reason to stay. The migration cost is 1–2 days for a typical project, while the ongoing cost of manual waiting, no cross-browser coverage, and no test runner adds up to weeks per year in maintenance overhead.
Frequently Asked Questions
Is Playwright better than Puppeteer in 2026?
For testing, yes. Playwright offers auto-waiting, a built-in test runner, multi-browser support (Chromium, Firefox, WebKit), API testing, and AI integration via MCP Server. Puppeteer remains a solid choice for lightweight Chromium-only scripting and web scraping where its smaller footprint is an advantage.
Can I migrate from Puppeteer to Playwright easily?
Yes. The migration is straightforward because Playwright's API was designed by the same engineers who created Puppeteer. Most API methods have direct equivalents. The key changes are replacing page.$() with page.locator(), removing waitForSelector calls (auto-waiting handles it), and switching to Playwright Test's fixture system. A typical 50-file migration takes 1–2 days.
Does Puppeteer support Firefox and Safari?
Puppeteer added experimental Firefox support, but it remains limited and not recommended for production testing. Safari (WebKit) is not supported at all. Playwright natively supports Chromium, Firefox, and WebKit (Safari's engine) with first-class API parity across all three.
Which is faster, Playwright or Puppeteer?
In single-script benchmarks, Puppeteer is marginally faster (~5–10%) because it has less abstraction overhead. However, Playwright's built-in parallel execution with worker isolation makes it significantly faster for test suites. A 200-test suite runs 3–5x faster in Playwright Test than the same tests in Puppeteer + Jest with manual parallelism.
Should I learn Puppeteer or Playwright as a beginner?
Learn Playwright. It has better documentation, a built-in test runner, codegen for recording tests, a VS Code extension with debugging, and a much larger job market in 2026. Puppeteer knowledge transfers easily from Playwright since the APIs share common ancestry, but starting with Playwright gives you more tools out of the box.
Asim Noaman
Senior QA Automation Engineer & AI Testing Specialist
With years of hands-on experience building test automation frameworks for production applications, Asim specializes in combining traditional QA methodologies with cutting-edge AI tools. He has helped teams adopt Playwright and AI-driven testing workflows to ship faster with fewer bugs.