---
title: "Playwright vs Puppeteer: which one in 2026"
description: "Playwright vs Puppeteer: browser coverage, test runner, auto-waiting and API differences, with the same scraping task and browser test in both."
canonical: "https://momentic.ai/blog/playwright-vs-puppeteer"
last-updated: "2026-10-31T09:50:51Z"
---

# Playwright vs Puppeteer: which one in 2026

URL: https://momentic.ai/blog/playwright-vs-puppeteer

[blog](/blog) [/ resources](/blog/category/resources)  / playwright-vs-puppeteer

Resources

Playwright and Puppeteer compared on browsers, test runner and waiting, with the same scraping script and browser test written in each.

Wei-Wei Wu

CEO, Momentic

## TL;DR

- Choose Playwright for a cross-browser test suite. Its API runs against Chromium, Firefox, and WebKit, and Playwright Test provides a built-in runner.
- Choose Puppeteer for Chrome-focused scripts, such as PDF generation or scraping, when you do not need a bundled test runner. Puppeteer also supports Firefox through WebDriver BiDi, but not WebKit.
- Playwright locators automatically wait for elements to become actionable before clicking or filling them. Puppeteer provides explicit waiting APIs, so scripts often need additional wait calls.

## Playwright vs Puppeteer at a glance

If you need to test across browser engines, Playwright gives you one API and a bundled test runner. Puppeteer fits Chrome-focused scripts, especially when you already use a separate test runner.

|  | Playwright | Puppeteer |
| --- | --- | --- |
| Browser support | Chromium, Firefox, and WebKit | Chrome and Firefox through WebDriver BiDi. No WebKit support. |
| Languages | JavaScript, TypeScript, Python, Java, and .NET | JavaScript and TypeScript |
| Test runner | Playwright Test includes fixtures, parallel runs, and retries | Bring your own runner, such as Jest or Mocha |
| Waiting before actions | Locators wait for actionability checks, including visibility and whether an element can receive events | Locator actions wait for conditions, but lower-level calls often require explicit waits |
| Install footprint | Browser downloads depend on which engines you install | Puppeteer normally downloads Chrome. puppeteer-core skips the browser download |
| Maintainer | Microsoft | Google |

Install size depends on the package and browser downloads you choose, so neither package name alone tells you how much disk space a CI job will use.

## Browser engine coverage: the decision most teams actually face

Playwright lets you run the same browser automation code against Chromium, Firefox, and WebKit. Its browser-specific drivers sit behind one API, so a test suite can check behavior across those engines without changing how it locates elements or performs actions. WebKit coverage helps catch engine-specific regressions, though Playwright’s WebKit build is not the Safari application.

Puppeteer began as a Chrome and Chromium automation library using the Chrome DevTools Protocol, or CDP. Puppeteer also supports Firefox through WebDriver BiDi, a browser automation protocol that Firefox implements. Puppeteer does not support WebKit, so Firefox support does not give a Puppeteer suite Safari-engine coverage.

If you need to catch regressions across all three engines, Playwright gives you that coverage in one suite. If your automation targets Chrome or Chromium, Puppeteer’s narrower browser support may be enough.

## Test runner and auto-waiting: what you get out of the box

Playwright Test gives you a runner for a browser test suite. It runs tests in parallel, provides fixtures for setup, supports configurable retries, and can record traces for debugging failures. Puppeteer provides browser automation APIs but no bundled test runner. You can use Jest, Mocha, or Vitest, but you supply the test lifecycle and runner configuration.

Playwright also checks whether an element can receive an action before acting on it. Before a locator click, it waits for the element to be visible, stable, enabled, and able to receive pointer events. A locator fill waits for a visible, enabled, editable element. Those checks reduce the need to place explicit waits before common interactions.

Puppeteer does have waiting APIs, including `waitForSelector`, and its Locator API performs actionability checks. With lower-level calls such as `page.click`, you more often add an explicit wait when the page creates an element asynchronously. The practical difference depends on which Puppeteer API you use, not on whether Puppeteer can wait at all.

## The same scraping task in both

Both scripts navigate to the same HTML fixture, wait for a heading, extract its text, and close the browser. The fixture keeps the example runnable without depending on a live site. Replace the data URL and `h1` selector with your target page and selector.

### Puppeteer

`npm init -y npm install puppeteer node scrape-puppeteer.mjs`

Save this as `scrape-puppeteer.mjs`.

`import assert from 'node:assert/strict'; import puppeteer from 'puppeteer';  const browser = await puppeteer.launch();  try {  const page = await browser.newPage();  await page.goto('data:text/html,<h1>Example%20Domain</h1>');   await page.waitForSelector('h1', { visible: true });  const title = await page.$eval('h1', element =>  element.textContent.trim()  );   assert.equal(title, 'Example Domain');  console.log(title); } finally {  await browser.close(); }`

### Playwright

`npm init -y npm install playwright npx playwright install chromium node scrape-playwright.mjs`

Save this as `scrape-playwright.mjs`.

`import assert from 'node:assert/strict'; import { chromium } from 'playwright';  const browser = await chromium.launch();  try {  const page = await browser.newPage();  await page.goto('data:text/html,<h1>Example%20Domain</h1>');   const heading = page.locator('h1');  await heading.waitFor({ state: 'visible' });  const title = (await heading.innerText()).trim();   assert.equal(title, 'Example Domain');  console.log(title); } finally {  await browser.close(); }`

Both scripts explicitly wait for a visible heading, so they make the same timing assumption. Puppeteer’s `$eval` queries the DOM when it runs and would fail if the element had not appeared. Playwright’s locator resolves when you call `innerText()` and can wait for the element to appear. Neither script uses a test runner.

## The same browser test in both

Both tests submit the same login form and check the same response. Playwright Test supplies the runner and a text assertion that waits for the expected value. Puppeteer needs a runner, an explicit wait, and an assertion library. The local fixture keeps network behavior out of the comparison.

Create `login.html`.

`<p id="result"></p>`

Install both tools. Puppeteer downloads Chrome during installation, while Playwright needs a separate browser-install command.

`npm init -y npm install -D @playwright/test puppeteer mocha npx playwright install chromium`

Create `playwright.config.mjs` so Playwright Test runs only its own spec.

`import { defineConfig } from '@playwright/test';  export default defineConfig({  testDir: './tests',  testMatch: '**/*.spec.mjs',  use: { browserName: 'chromium' }, });`

Save the Playwright test as `tests/login.spec.mjs`.

`import { test, expect } from '@playwright/test'; import { pathToFileURL } from 'node:url'; import { resolve } from 'node:path';  test('accepts valid credentials', async ({ page }) => {  await page.goto(pathToFileURL(resolve('login.html')).href);  await page.locator('#username').fill('ada');  await page.locator('#password').fill('secret');  await page.locator('#submit').click();  await expect(page.locator('#result')).toHaveText('Welcome, Ada'); });`

Run it with `npx playwright test`. Playwright Test launches the browser, provides the page fixture, and closes it after the test.

Save the Puppeteer test as `tests/login.test.mjs`. Mocha supplies the runner, while Node supplies the assertion.

`import { describe, it } from 'mocha'; import assert from 'node:assert/strict'; import puppeteer from 'puppeteer'; import { pathToFileURL } from 'node:url'; import { resolve } from 'node:path';  describe('login', () => {  it('accepts valid credentials', async () => {  const browser = await puppeteer.launch();  try {  const page = await browser.newPage();  await page.goto(pathToFileURL(resolve('login.html')).href);  await page.type('#username', 'ada');  await page.type('#password', 'secret');  await page.click('#submit');  await page.waitForFunction(  () => document.querySelector('#result')?.textContent === 'Welcome, Ada'  );  const result = await page.$eval('#result', element => element.textContent);  assert.equal(result, 'Welcome, Ada');  } finally {  await browser.close();  }  }); });`

Run it with `npx mocha --timeout 30000 tests/login.test.mjs`. Mocha provides the test lifecycle, but the Puppeteer script manages its browser and waits for the result explicitly.

## When Puppeteer is the right call

Puppeteer fits when your automation runs in Chrome or Chromium and you do not need a cross-browser test suite. A headless Chrome worker that scrapes pages or generates PDFs can use Puppeteer without adding a test runner. Chrome extension tests are another fit when the extension's behavior in Chrome is the target.

Puppeteer also makes sense if your existing tooling uses Chrome DevTools Protocol sessions or Lighthouse. You can keep Chrome-specific inspection and browser control in the same scripts rather than changing tools for a task that does not require Firefox or WebKit.

For a container that already supplies Chrome, `puppeteer-core` avoids downloading another browser during installation. Playwright offers `playwright-core` for a similar setup, so compare the dependencies in your actual image before treating install size as the deciding factor.

## When Playwright is the right call

Choose Playwright when your end-to-end suite must catch regressions across Chromium, Firefox, and WebKit. You can run the same tests against each browser engine instead of maintaining separate browser automation code.

Playwright Test also suits a growing suite. Its built-in runner can execute tests in parallel, while locator actions wait for elements to become actionable before clicking or typing. Those features reduce the runner setup and explicit waiting code you would otherwise maintain around Puppeteer. For a one-off Chrome script, that extra test infrastructure may offer little benefit.

## Where both tools still leave engineering teams exposed

Playwright and Puppeteer can drive a browser, but you still own the test suite around them. When a checkout button changes, someone must determine whether a failed test points to a broken purchase flow or an outdated locator. Playwright’s auto-waiting can handle a button that appears late. It cannot decide whether the button now performs the right action.

A coding agent can write tests and propose fixes, but using one with either tool remains a build decision. You still need rules for reviewing changed assertions, reproducing CI failures, and deciding which failures represent regressions. Replacing a locator without checking the intended behavior can make a test pass for the wrong reason. Playwright’s [maintenance limitations](/blog/what-are-the-limitations-of-playwright) deserve separate consideration when you plan a larger suite.

If you would rather buy support for that maintenance work, Momentic offers [plain-English YAML tests and self-healing](/docs). Those capabilities address a different question than whether Playwright or Puppeteer provides the better browser API for your code.

## Frequently asked questions

Can Puppeteer test Firefox and Safari?    Puppeteer can automate Firefox through WebDriver BiDi, but it does not support Safari or WebKit. Playwright supports Chromium, Firefox, and WebKit. If Safari coverage drives your choice, compare [other Puppeteer alternatives](/blog/puppeteer-alternatives) too.    Does Puppeteer have a test runner?    Puppeteer provides browser automation APIs, not a bundled test runner. You can run its scripts with Jest, Mocha, or another runner. Playwright includes Playwright Test.    Is Playwright faster than Puppeteer?    Neither tool has a universal speed advantage. The browser, page, waits, and test configuration affect runtime. Benchmark your own task before choosing on speed alone.    Can you migrate a Puppeteer script to Playwright?    Yes, but expect API changes rather than a drop-in replacement. For example, a Puppeteer script that calls waitForSelector before clicking may use a Playwright locator, which waits for the element to become actionable. See [Playwright alternatives](/blog/playwright-alternatives) if migration is part of a broader tool review.    Which has better TypeScript support?    Both support TypeScript. Playwright also provides a TypeScript-ready test runner, which reduces setup for a new test suite. If you are comparing runner and browser support across frameworks, see [Selenium vs. Playwright](/blog/selenium-vs-playwright).

Still have additional questions?

## Keep reading.

[Resources   Testing iOS and Android apps with Claude Code     Four ways to test iOS and Android apps with Claude Code, from simctl and adb to XCUITest, Espresso, mobile MCP servers and Momentic mobile tests.     Wei-Wei Wu     12 min read](/blog/claude-code-mobile-app-testing)[Resources   UI and end-to-end testing with OpenAI Codex     How far Codex gets on UI and end-to-end tests, and how AGENTS.md instructions plus a test runner over MCP turn its output into checks you can trust.     Wei-Wei Wu     8 min read](/blog/codex-ui-testing-guide)[Resources   Migrating from Appium to Maestro     How to move an Appium suite to Maestro flows, with a worked login example, a concept map and the cases where Appium is still the better fit.     Wei-Wei Wu     6 min read](/blog/migrate-appium-to-maestro)

## Close the feedback loop.

Point Momentic at your app. Free to start, no credit card.

[Try for free](https://app.momentic.ai/signup) [Contact sales](/sales)
