Short verdict: pick Puppeteer for web scraping, PDF generation and Chrome-only automation. Pick Playwright for cross-browser E2E testing: it drives Chromium, Firefox and WebKit with one API and ships its own test runner, auto-waiting and trace viewer.
| Criterion | Puppeteer | Playwright |
|---|---|---|
| Browsers | Chrome/Chromium first, no WebKit/Safari | Chromium, Firefox and WebKit with one API |
| Test runner | None built in; usually paired with Jest | Built-in @playwright/test: assertions, parallel runs, retries, traces |
| Waiting | Explicit waitForSelector calls are often needed | Auto-waits for elements before actions |
| Debugging | slowMo, DevTools, screenshots, console logs | Inspector, trace viewer, codegen |
| Browser download | npm install puppeteer downloads Chrome for Testing | Separate step: npx playwright install |
| Screenshot from the CLI | No CLI command, use page.screenshot() | npx playwright screenshot <url> <file> |
| Best fit | Scraping, PDF generation, Chrome-only tasks | Cross-browser E2E and visual regression tests |
TL;DR
- Choose Puppeteer for: web scraping, PDF generation, Chrome-specific automation, lightweight tasks
- Choose Playwright for: E2E testing, cross-browser coverage, built-in test runner, advanced debugging
- The basic APIs are similar (Playwright was built by former Puppeteer engineers), so migration is straightforward
- Playwright has more weekly npm downloads: about 117 million vs about 13 million for Puppeteer in the week of September 22–28, 2026 (npm: playwright, puppeteer)
Reading time: 18 minutes
Puppeteer and Playwright are the two dominant Node.js browser automation tools, but they have fundamentally diverged in purpose: Puppeteer excels at Chrome-specific tasks while Playwright has become the leading framework for cross-browser E2E testing. Puppeteer, released by Google in 2017, pioneered direct Chrome DevTools Protocol automation and is widely used for web scraping and headless Chrome tasks. Playwright was built by Microsoft in 2020 with a team of former Puppeteer engineers. As of September 2026, Playwright has about 97,000 GitHub stars and Puppeteer about 96,000. In the week of September 22–28, 2026, npm recorded about 117 million downloads of playwright and about 76 million of @playwright/test, against about 13 million of puppeteer and about 25 million of puppeteer-core (npm registry API: playwright, @playwright/test, puppeteer, puppeteer-core). Playwright patches browsers at build time to deliver consistent APIs across Chromium, Firefox, and WebKit. Puppeteer focuses on Chrome and, since v23, also supports Firefox through WebDriver BiDi (Puppeteer FAQ).
Introduction
Browser automation has evolved significantly from the WebDriver era. Puppeteer (as discussed in Percy, Applitools & BackstopJS: Visual Regression Testing Solutions Compared), released by Google in 2017, demonstrated that direct CDP access could provide faster, more reliable automation than WebDriver-based tools. Building on this foundation, Microsoft’s Playwright team (many former Puppeteer contributors) launched Playwright in 2020 with ambitious goals: true cross-browser support, enhanced debugging capabilities, and a more opinionated testing framework.
Key Questions This Article Answers:
- What are the fundamental architectural differences?
- When should you choose Puppeteer vs Playwright?
- How do they compare in real-world scenarios?
- What are the migration considerations?
Historical Context and Origins
Puppeteer’s Genesis
Release: January 2017 by Google Chrome team
Original Goals:
- Provide high-level API over Chrome DevTools Protocol
- Enable headless Chrome automation
- Support web scraping and PDF generation
- Offer simpler alternative to Selenium
Evolution:
- Initially Chrome-only
- Added Firefox support: since v23.0.0 Puppeteer supports stable Firefox, automated through WebDriver BiDi (Puppeteer FAQ)
- Focused on Chrome/Chromium as primary target
- Maintained by Google Chrome team
Playwright’s Emergence
Release: January 2020 by Microsoft
Team Background:
- Led by former Puppeteer core contributors
- Engineers who built Puppeteer at Google
Design Philosophy:
- Cross-browser from day one (Chromium, Firefox, WebKit)
- Built for testing, not just automation
- Comprehensive tracing and debugging
- Automatic waiting and resilience
Architectural Comparison
Protocol Layer
Puppeteer:
Puppeteer API → Chrome DevTools Protocol → Chrome
→ WebDriver BiDi → Firefox (default), Chrome (optional)
Playwright:
Playwright API → Playwright Protocol → Browser-specific patches
→ Chromium (CDP)
→ Firefox (Juggler)
→ WebKit (Custom protocol)
Key Difference: Playwright patches browsers at build time, enabling consistent APIs across all browsers. Puppeteer relies on browser-native protocols.
Browser Support Matrix
| Feature | Puppeteer | Playwright |
|---|---|---|
| Chromium | ✅ Full support | ✅ Full support |
| Chrome | ✅ Full support | ✅ Full support |
| Firefox | ✅ Supported since v23, via WebDriver BiDi (pptr.dev) | ✅ Full support |
| WebKit/Safari | ❌ Not supported | ✅ Full support |
| Edge | ✅ Via Chromium | ✅ Full support |
| Protocol | CDP for Chrome, WebDriver BiDi for Firefox | Custom patches |
| Browser Versioning | Each release is tied to a specific Chrome for Testing version | Each release is tied to specific browser builds |
Installation and Setup
Puppeteer:
npm install puppeteer
# Downloads Chrome for Testing automatically
npm install puppeteer-core
# No browser download, use system Chrome
Playwright:
npm install playwright
npx playwright install # Downloads all browsers
npm install @playwright/test # With test runner
npx playwright install chromium # Single browser
For what each tool actually downloads and how much disk space it takes, see Install size: Puppeteer vs Playwright.
API and Developer Experience
Basic Automation
Puppeteer:
const puppeteer = require('puppeteer');
(async () => {
const browser = await puppeteer.launch({ headless: false });
const page = await browser.newPage();
await page.goto('https://example.com');
await page.type('#username', 'testuser');
await page.type('#password', 'password123');
await page.click('button[type="submit"]');
await page.waitForSelector('.dashboard');
const title = await page.title();
console.log('Page title:', title);
await browser.close();
})();
Playwright:
const { chromium } = require('playwright');
(async () => {
const browser = await chromium.launch({ headless: false });
const page = await browser.newPage();
await page.goto('https://example.com');
await page.fill('#username', 'testuser');
await page.fill('#password', 'password123');
await page.click('button[type="submit"]');
await page.waitForSelector('.dashboard');
const title = await page.title();
console.log('Page title:', title);
await browser.close();
})();
API Similarity: the two scripts are almost identical. The basic APIs are close because Playwright was built by former Puppeteer engineers, but Playwright offers additional features.
Advanced Selectors
Puppeteer:
// CSS selectors
await page.click('#submit-button');
await page.click('.form-control:nth-child(2)');
// XPath
await page.waitForXPath('//button[contains(text(), "Submit")]');
const [button] = await page.$x('//button[@type="submit"]');
await button.click();
// Custom text selector (via page.evaluate)
await page.evaluate(() => {
const button = Array.from(document.querySelectorAll('button'))
.find(b => b.textContent.includes('Submit'));
button.click();
});
Playwright:
// CSS selectors
await page.click('#submit-button');
// Text selector
await page.click('text=Submit');
await page.click('button:has-text("Submit")');
// XPath
await page.click('xpath=//button[@type="submit"]');
// Advanced selectors
await page.click('button >> text=Submit'); // Chaining
await page.click('button:right-of(:text("Username"))'); // Layout-based
await page.click('button:below(#header)');
await page.click('button:near(.form-group)');
// Role-based (accessibility)
await page.click('role=button[name="Submit"]');
Winner: Playwright - richer selector engine with layout-based and role-based selectors.
Auto-Waiting Behavior
Puppeteer:
// Manual waiting often needed
await page.waitForSelector('#result');
await page.waitForFunction(() => document.querySelector('#result').textContent !== '');
// Network idle waiting
await page.goto('https://example.com', { waitUntil: 'networkidle2' });
// Custom waits
await page.waitForTimeout(1000);
await page.waitForFunction(
() => window.appReady === true,
{ timeout: 5000 }
);
Playwright:
// Automatic waiting built-in
await page.click('#result'); // Waits for element automatically
// Auto-waits for:
// - Element to be visible
// - Element to be stable (not animating)
// - Element to be enabled
// - Element to receive events
// Still supports explicit waits
await page.waitForSelector('#result', { state: 'visible' });
await page.waitForLoadState('networkidle');
await page.waitForFunction(() => window.appReady === true);
Winner: Playwright - more aggressive and intelligent automatic waiting reduces need for explicit waits.
Multiple Contexts and Parallel Execution
Puppeteer:
const browser = await puppeteer.launch();
// Incognito contexts
const context1 = await browser.createIncognitoBrowserContext();
const context2 = await browser.createIncognitoBrowserContext();
const page1 = await context1.newPage();
const page2 = await context2.newPage();
await Promise.all([
page1.goto('https://example.com'),
page2.goto('https://example.com')
]);
// Isolated storage, cookies, sessions
Playwright:
const browser = await chromium.launch();
// Browser contexts (like incognito)
const context1 = await browser.newContext({
viewport: { width: 1920, height: 1080 },
userAgent: 'Custom User Agent',
locale: 'en-US',
timezoneId: 'America/New_York',
permissions: ['geolocation']
});
const context2 = await browser.newContext({
viewport: { width: 375, height: 667 }, // Mobile
isMobile: true,
hasTouch: true
});
// Parallel execution with different contexts
const [page1, page2] = await Promise.all([
context1.newPage(),
context2.newPage()
]);
Winner: Playwright - more configuration options for contexts, better multi-context support.
Cross-Browser Testing
Browser Consistency
Puppeteer:
- Chrome/Chromium: Excellent, native support
- Firefox: Supported since v23 through WebDriver BiDi; some Puppeteer features are not available over BiDi (list)
- Safari: Not supported
Playwright:
const { chromium, firefox, webkit } = require('playwright');
async function testAllBrowsers() {
for (const browserType of [chromium, firefox, webkit]) {
const browser = await browserType.launch();
const page = await browser.newPage();
await page.goto('https://example.com');
// Same API across all browsers
await page.screenshot({ path: `screenshot-${browserType.name()}.png` });
await browser.close();
}
}
Real-World Cross-Browser Test:
const { test, expect } = require('@playwright/test');
test.describe('Cross-browser login', () => {
test('should login successfully', async ({ page, browserName }) => {
await page.goto('https://example.com/login');
await page.fill('#username', 'testuser');
await page.fill('#password', 'password123');
await page.click('button[type="submit"]');
await expect(page.locator('.dashboard')).toBeVisible();
// Browser-specific assertions
if (browserName === 'webkit') {
// Safari-specific checks
await expect(page.locator('.webkit-notice')).toBeVisible();
}
});
});
Winner: Playwright - comprehensive, consistent cross-browser support is core feature.
Network Interception and Mocking
Request Interception
Puppeteer:
await page.setRequestInterception(true);
page.on('request', (request) => {
if (request.resourceType() === 'image') {
request.abort();
} else if (request.url().includes('/api/users')) {
request.respond({
status: 200,
contentType: 'application/json',
body: JSON.stringify([
{ id: 1, name: 'Test User' }
])
});
} else {
request.continue();
}
});
await page.goto('https://example.com');
Playwright:
// Route-based interception
await page.route('**/*.{png,jpg,jpeg}', route => route.abort());
await page.route('**/api/users', route => {
route.fulfill({
status: 200,
contentType: 'application/json',
body: JSON.stringify([
{ id: 1, name: 'Test User' }
])
});
});
// Or modify requests
await page.route('**/api/**', route => {
const headers = route.request().headers();
headers['Authorization'] = 'Bearer fake-token';
route.continue({ headers });
});
await page.goto('https://example.com');
HAR Recording
Puppeteer:
// Requires third-party library
const PuppeteerHar = require('puppeteer-har');
const har = new PuppeteerHar(page);
await har.start({ path: 'output.har' });
await page.goto('https://example.com');
await har.stop();
Playwright:
// Built-in HAR support
const context = await browser.newContext({
recordHar: { path: 'output.har' }
});
const page = await context.newPage();
await page.goto('https://example.com');
await context.close(); // HAR automatically saved
Winner: Playwright - more flexible routing, built-in HAR recording.
Testing Framework Integration
Puppeteer with Jest
// jest-puppeteer.config.js
module.exports = {
launch: {
headless: true,
args: ['--no-sandbox']
},
browserContext: 'default'
};
// test.spec.js
describe('Login Test', () => {
beforeAll(async () => {
await page.goto('https://example.com');
});
it('should display login form', async () => {
await expect(page).toMatch('Login');
const title = await page.title();
expect(title).toBe('Login - Example');
});
it('should login successfully', async () => {
await page.type('#username', 'testuser');
await page.type('#password', 'password123');
await page.click('button[type="submit"]');
await page.waitForSelector('.dashboard');
const url = page.url();
expect(url).toContain('/dashboard');
});
});
Playwright Test Runner
// playwright.config.js
module.exports = {
testDir: './tests',
timeout: 30000,
retries: 2,
use: {
headless: true,
viewport: { width: 1920, height: 1080 },
screenshot: 'only-on-failure',
video: 'retain-on-failure',
trace: 'on-first-retry'
},
projects: [
{ name: 'chromium', use: { browserName: 'chromium' } },
{ name: 'firefox', use: { browserName: 'firefox' } },
{ name: 'webkit', use: { browserName: 'webkit' } }
]
};
// test.spec.js
const { test, expect } = require('@playwright/test');
test.describe('Login Test', () => {
test.beforeEach(async ({ page }) => {
await page.goto('https://example.com');
});
test('should display login form', async ({ page }) => {
await expect(page.locator('h1')).toHaveText('Login');
await expect(page).toHaveTitle('Login - Example');
});
test('should login successfully', async ({ page }) => {
await page.fill('#username', 'testuser');
await page.fill('#password', 'password123');
await page.click('button[type="submit"]');
await expect(page).toHaveURL(/.*dashboard/);
await expect(page.locator('.dashboard')).toBeVisible();
});
});
Winner: Playwright - opinionated test runner with built-in assertions, parallelization, retries, screenshots, videos, and traces.
Debugging Capabilities
Puppeteer Debugging
// Slowmo
const browser = await puppeteer.launch({
headless: false,
slowMo: 100 // Slow down by 100ms
});
// Devtools
const browser = await puppeteer.launch({
headless: false,
devtools: true
});
// Screenshots
await page.screenshot({ path: 'screenshot.png', fullPage: true });
// Console logs
page.on('console', msg => console.log('PAGE LOG:', msg.text()));
// Page errors
page.on('pageerror', error => console.log('PAGE ERROR:', error.message));
Playwright Debugging
// Inspector (interactive debugging)
// Set environment variable
// PWDEBUG=1 npm test
// Or in code
await page.pause(); // Opens inspector
// Trace viewer (time-travel debugging)
const context = await browser.newContext({
recordVideo: { dir: 'videos/' },
recordTrace: { dir: 'traces/' }
});
// View trace: npx playwright show-trace trace.zip
// Codegen (record actions)
// npx playwright codegen https://example.com
// Headed mode with slomo
const browser = await chromium.launch({
headless: false,
slowMo: 1000
});
Playwright Inspector:
# Interactive debugging with:
# - Step through actions
# - Inspect selectors
# - View console logs
# - Edit and continue
PWDEBUG=1 npx playwright test
Playwright Trace Viewer:
// Record trace
const context = await browser.newContext();
await context.tracing.start({ screenshots: true, snapshots: true });
// ... perform actions ...
await context.tracing.stop({ path: 'trace.zip' });
// View: npx playwright show-trace trace.zip
// Shows:
// - Action log with timing
// - DOM snapshots at each step
// - Network activity
// - Console logs
// - Screenshots
Winner: Playwright - trace viewer provides time-travel debugging, inspector is more polished.
Performance Comparison
This article does not publish benchmark numbers: timings depend on the machine, the network and the page under test. To compare the tools on your own application, time the same scenario in both.
How to measure it yourself
Test Scenario: Navigate to page, fill form, submit, wait for result
Puppeteer:
console.time('test');
const browser = await puppeteer.launch();
const page = await browser.newPage();
await page.goto('https://example.com/form');
await page.type('#name', 'John Doe');
await page.type('#email', 'john@example.com');
await page.click('button[type="submit"]');
await page.waitForSelector('.success-message');
await browser.close();
console.timeEnd('test');
Playwright:
console.time('test');
const browser = await chromium.launch();
const page = await browser.newPage();
await page.goto('https://example.com/form');
await page.fill('#name', 'John Doe');
await page.fill('#email', 'john@example.com');
await page.click('button[type="submit"]');
await page.waitForSelector('.success-message');
await browser.close();
console.timeEnd('test');
What affects the result:
- On Chromium both tools drive the browser over the Chrome DevTools Protocol
- Playwright’s auto-waiting removes the need for fixed
waitForTimeoutdelays - Parallel execution: Playwright’s test runner handles it out of the box; with Puppeteer it depends on the runner you pair it with, such as Jest
Mobile and Device Emulation
Puppeteer
const iPhone = puppeteer.devices['iPhone 12'];
await page.emulate(iPhone);
// Or custom
await page.setViewport({ width: 375, height: 667, isMobile: true });
await page.setUserAgent('Mozilla/5.0 (iPhone...');
Playwright
const { devices } = require('playwright');
const iPhone12 = devices['iPhone 12'];
const context = await browser.newContext({
...iPhone12,
locale: 'en-US',
geolocation: { latitude: 40.7128, longitude: -74.0060 },
permissions: ['geolocation']
});
// Or Playwright's mobile browsers (Android)
const { android } = require('playwright');
const [device] = await android.devices();
await device.installApk('app.apk');
const context = await device.launchBrowser();
Winner: Playwright - native Android device support, more comprehensive device emulation options.
Install size: Puppeteer vs Playwright
Neither npm package contains a browser. The disk footprint comes from the browser binaries, and the two tools differ in which browsers they download and when.
| Puppeteer | Playwright | |
|---|---|---|
| Browsers downloaded by default | Chrome for Testing and chrome-headless-shell | Chromium, a separate Chromium headless shell, Firefox and WebKit |
| When the download happens | During npm i puppeteer (install script) | Only when you run npx playwright install |
| Size stated in the official docs | Chrome for Testing: ~170 MB macOS, ~282 MB Linux, ~280 MB Windows (pptr.dev) | macOS example: 281M chromium, 187M firefox, 180M webkit (playwright.dev) |
| Default browser cache | ~/.cache/puppeteer | ~/Library/Caches/ms-playwright (macOS), ~/.cache/ms-playwright (Linux), %USERPROFILE%\AppData\Local\ms-playwright (Windows) |
| Skip the download | PUPPETEER_SKIP_DOWNLOAD, or install puppeteer-core | Do not run npx playwright install |
| Use a browser already on the machine | channel: 'chrome' or executablePath | channel: 'chrome' or channel: 'msedge' |
What Puppeteer downloads
According to the Puppeteer installation guide, installing puppeteer automatically downloads a recent version of Chrome for Testing (~170 MB macOS, ~282 MB Linux, ~280 MB Windows) and a chrome-headless-shell binary (starting with v21.6.0). The browsers go to $HOME/.cache/puppeteer by default.
- The download can be skipped silently. The same guide warns that package managers which block dependency install scripts by default (it lists pnpm, Yarn Berry, Bun and Deno) skip the browser download, and Puppeteer then fails with
Could not find Chrome. Runnpx puppeteer browsers installto download the browser manually. - Skipping on purpose. Set
PUPPETEER_SKIP_DOWNLOAD(orskipDownloadin the config file), or installpuppeteer-core, which “will not download Chrome when installed”. The cache location is changed withPUPPETEER_CACHE_DIRorcacheDirectory(Configuration reference). - System Chrome. With
puppeteer-coreyou passexecutablePath, orchannel, which makes Puppeteer look for a regular Chrome installation at a known system location (LaunchOptions). - Firefox is opt-in. Firefox has
skipDownload: trueby default; enable it in the config file and runnpx puppeteer browsers install(Configuration guide).
npm i puppeteer # package + Chrome for Testing + chrome-headless-shell
PUPPETEER_SKIP_DOWNLOAD=true npm i puppeteer # package only, no browser download
npm i puppeteer-core # library only, never downloads a browser
npx puppeteer browsers install # download the browsers manually
// puppeteer-core with the Chrome already installed on the machine
const puppeteer = require('puppeteer-core');
const browser = await puppeteer.launch({ channel: 'chrome' });
What Playwright downloads
npm i -D playwright installs only the library. The Playwright library docs state that you also need to install browsers, either with npx playwright install or by adding the helper packages @playwright/browser-chromium, @playwright/browser-firefox and @playwright/browser-webkit, which download a browser during npm install.
Per the Browsers guide, running npx playwright install without arguments installs the default browsers. Playwright downloads Chromium, WebKit and Firefox into an OS-specific cache folder, and ships a separate Chromium headless shell for headless mode. The guide says these browsers “will take a few hundred megabytes of disk space” and shows this macOS example: 281M for chromium, 187M for firefox, 180M for webkit.
- Install less.
npx playwright install chromiumdownloads a single browser.--only-shellskips the full Chromium build when you only run headless, and--no-shellskips the headless shell. - System Chrome or Edge. Playwright can run against branded Google Chrome and Microsoft Edge available on the machine through the
channeloption; it does not install them by default. - Cache location.
PLAYWRIGHT_BROWSERS_PATHmoves the cache;PLAYWRIGHT_BROWSERS_PATH=0places binaries innode_modules/playwright-core/.local-browsers. - Cleanup. Playwright garbage-collects browser versions that no installed Playwright needs anymore;
npx playwright uninstallremoves the browsers of the current installation. - Preview without downloading.
npx playwright install --dry-rundoes not execute the installation and only prints information: each browser, its install location and download URL. The flag is defined in the Playwright CLI source. It does not print sizes.
npm i -D playwright # library only, no browsers
npx playwright install # default browsers: Chromium, Firefox, WebKit
npx playwright install chromium # a single browser
npx playwright install --only-shell # Chromium headless shell only, no full Chromium
npx playwright install --no-shell # skip the Chromium headless shell
npx playwright install --dry-run # print what would be installed, download nothing
npx playwright install --list # list browsers from all Playwright installations
npx playwright uninstall # remove browsers of the current installation
// Google Chrome already installed on the machine
const { chromium } = require('playwright');
const browser = await chromium.launch({ channel: 'chrome' }); // or 'msedge'
Measure it on your machine
The figures above are examples from the documentation, and browser builds change with every release. For a Chromium-only setup the documented numbers are in the same range for both tools; Playwright’s footprint grows when you add Firefox and WebKit. To get the real numbers for your OS and versions:
du -sh ~/.cache/puppeteer/* # Puppeteer browsers (default cache)
du -hs ~/Library/Caches/ms-playwright/* # Playwright browsers, macOS
du -hs ~/.cache/ms-playwright/* # Playwright browsers, Linux
du -sh node_modules # npm packages only (default browser caches live outside node_modules)
npx playwright install --dry-run # what Playwright would download and where
Taking screenshots from Node.js and the CLI
Both libraries take screenshots with page.screenshot(). The difference is on the command line: Playwright has a screenshot command, Puppeteer does not.
Puppeteer (API)
const puppeteer = require('puppeteer');
(async () => {
const browser = await puppeteer.launch();
const page = await browser.newPage();
await page.goto('https://example.com', { waitUntil: 'networkidle2' });
await page.screenshot({ path: 'page.png' }); // visible viewport
await page.screenshot({ path: 'full.png', fullPage: true }); // full scrollable page
const heading = await page.waitForSelector('h1');
await heading.screenshot({ path: 'h1.png' }); // single element
await browser.close();
})();
Page.screenshot() and ElementHandle.screenshot() are described in the Puppeteer screenshots guide; fullPage is listed in ScreenshotOptions.
Playwright (API)
const { chromium } = require('playwright');
(async () => {
const browser = await chromium.launch();
const page = await browser.newPage();
await page.goto('https://example.com');
await page.screenshot({ path: 'page.png' }); // visible viewport
await page.screenshot({ path: 'full.png', fullPage: true }); // full scrollable page
await page.locator('h1').screenshot({ path: 'h1.png' }); // single element
const buffer = await page.screenshot(); // no path: returns a Buffer
await browser.close();
})();
These calls come from the Playwright screenshots guide.
Playwright CLI: a screenshot without writing a script
# Syntax: npx playwright screenshot [options] <url> <filename>
npx playwright screenshot https://example.com example.png
# Entire scrollable page
npx playwright screenshot --full-page https://example.com full.png
# Another browser engine: cr/chromium, ff/firefox, wk/webkit
npx playwright screenshot -b webkit https://example.com webkit.png
# Device emulation and dark color scheme
npx playwright screenshot --device "iPhone 11" --color-scheme dark https://example.com mobile.png
# Wait for an element and a timeout (ms) before capturing
npx playwright screenshot --wait-for-selector "h1" --wait-for-timeout 1000 https://example.com ready.png
# All options
npx playwright screenshot --help
The command and its options (--full-page, --wait-for-selector, --wait-for-timeout, -b/--browser, --device, --color-scheme) are defined in the Playwright CLI source, and npx playwright screenshot --help prints them for your installed version. The browser it uses must already be installed with npx playwright install.
Is there a Puppeteer equivalent? No. The puppeteer CLI has a single command, puppeteer browsers, which manages browser downloads. For a screenshot you write a short Node.js script like the one above and run it with node screenshot.js.
Real-World Use Case Comparison
Use Case 1: Web Scraping
Best Choice: Puppeteer
Reasoning:
- Lighter weight (a single Chrome for Testing download)
- Mature ecosystem for scraping
- Excellent documentation for scraping patterns
const puppeteer = require('puppeteer');
async function scrapeProducts() {
const browser = await puppeteer.launch();
const page = await browser.newPage();
await page.goto('https://example.com/products');
const products = await page.evaluate(() => {
return Array.from(document.querySelectorAll('.product')).map(el => ({
name: el.querySelector('.name').textContent,
price: el.querySelector('.price').textContent,
image: el.querySelector('img').src
}));
});
await browser.close();
return products;
}
Use Case 2: Cross-Browser E2E Testing
Best Choice: Playwright
Reasoning:
- True cross-browser support (Chromium, Firefox, WebKit)
- Built-in test runner with retry logic
- Trace viewer for debugging failures
- Parallel execution across browsers
const { test, expect } = require('@playwright/test');
test.describe('Checkout Flow', () => {
test('should complete purchase', async ({ page }) => {
await page.goto('https://example.com/product/123');
await page.click('text=Add to Cart');
await page.click('text=Checkout');
await page.fill('#card-number', '4242424242424242');
await page.fill('#expiry', '12/25');
await page.fill('#cvv', '123');
await page.click('text=Complete Purchase');
await expect(page.locator('.success-message')).toBeVisible();
await expect(page.locator('.order-number')).toContainText(/ORD-\d+/);
});
});
Use Case 3: PDF Generation
Best Choice: Puppeteer
Reasoning:
- More mature PDF generation
- Simpler API for PDF use cases
- Better documentation and examples
const puppeteer = require('puppeteer');
async function generateInvoice(data) {
const browser = await puppeteer.launch();
const page = await browser.newPage();
// Generate HTML from template
const html = `
<html>
<body>
<h1>Invoice #${data.invoiceNumber}</h1>
<p>Customer: ${data.customerName}</p>
<p>Amount: $${data.amount}</p>
</body>
</html>
`;
await page.setContent(html);
await page.pdf({
path: `invoice-${data.invoiceNumber}.pdf`,
format: 'A4',
printBackground: true,
margin: { top: '20px', right: '20px', bottom: '20px', left: '20px' }
});
await browser.close();
}
Use Case 4: Visual Regression Testing
Best Choice: Playwright
Reasoning:
- Built-in screenshot comparison
- Cross-browser visual testing
- Better threshold control
- Integrated with test runner
const { test, expect } = require('@playwright/test');
test.describe('Visual Regression', () => {
test('homepage should match snapshot', async ({ page }) => {
await page.goto('https://example.com');
await expect(page).toHaveScreenshot('homepage.png', {
maxDiffPixels: 100,
threshold: 0.2
});
});
test('should match across browsers', async ({ page, browserName }) => {
await page.goto('https://example.com');
await expect(page).toHaveScreenshot(`homepage-${browserName}.png`);
});
});
Migration Guide: Puppeteer to Playwright
API Mapping
| Puppeteer | Playwright |
|---|---|
puppeteer.launch() | chromium.launch() |
browser.newPage() | context.newPage() or browser.newPage() |
page.goto(url) | page.goto(url) |
page.$(selector) | page.locator(selector) |
page.$$(selector) | page.locator(selector).all() |
page.evaluate() | page.evaluate() |
page.type(selector, text) | page.fill(selector, text) |
page.click(selector) | page.click(selector) |
page.waitForSelector() | page.waitForSelector() or page.locator().waitFor() |
page.waitForNavigation() | page.waitForURL() or page.waitForLoadState() |
page.screenshot() | page.screenshot() |
page.pdf() | page.pdf() |
Step-by-Step Migration
1. Update Dependencies
npm uninstall puppeteer
npm install playwright @playwright/test
npx playwright install
2. Update Imports
// Before
const puppeteer = require('puppeteer');
// After
const { chromium } = require('playwright');
// or
const { test, expect } = require('@playwright/test');
3. Update Browser Launch
// Before
const browser = await puppeteer.launch({ headless: false });
const page = await browser.newPage();
// After
const browser = await chromium.launch({ headless: false });
const context = await browser.newContext();
const page = await context.newPage();
// Or simplified
const browser = await chromium.launch({ headless: false });
const page = await browser.newPage();
4. Update Element Selection
// Before
const element = await page.$('#submit');
await element.click();
// After (recommended)
await page.click('#submit');
// Or using locators
const element = page.locator('#submit');
await element.click();
5. Migrate to Test Runner (Optional but Recommended)
// Before: Custom test setup
const puppeteer = require('puppeteer');
describe('My Tests', () => {
let browser, page;
beforeAll(async () => {
browser = await puppeteer.launch();
page = await browser.newPage();
});
afterAll(async () => {
await browser.close();
});
test('test case', async () => {
await page.goto('https://example.com');
// assertions
});
});
// After: Playwright Test Runner
const { test, expect } = require('@playwright/test');
test.describe('My Tests', () => {
test('test case', async ({ page }) => {
await page.goto('https://example.com');
await expect(page.locator('h1')).toHaveText('Welcome');
});
});
Decision Matrix
Choose Puppeteer If:
- ✅ You only need Chrome/Chromium support
- ✅ You’re building web scrapers
- ✅ You need lightweight automation
- ✅ You’re generating PDFs or screenshots
- ✅ You have existing Puppeteer codebase
- ✅ You prefer minimal abstractions
Choose Playwright If:
- ✅ You need cross-browser testing (Firefox, Safari)
- ✅ You’re building E2E test suites
- ✅ You want integrated test runner
- ✅ You need advanced debugging (trace viewer)
- ✅ You want automatic waiting and retries
- ✅ You need mobile/tablet emulation
- ✅ You’re starting a new project
Conclusion
Both Puppeteer and Playwright are excellent browser automation tools, each excelling in different scenarios. Puppeteer remains the go-to choice for web scraping, PDF generation, and Chrome-specific automation where its maturity and simplicity shine. Playwright, however, has emerged as the superior solution for comprehensive E2E testing, offering genuine cross-browser support, powerful debugging capabilities, and an opinionated testing framework that addresses common pain points.
For new test automation projects, Playwright’s advantages—true cross-browser support, trace viewer, automatic waiting, and integrated test runner—make it the recommended choice. For scraping, PDF generation, or Chrome-only automation, Puppeteer’s lightweight approach and mature ecosystem remain compelling.
The good news: migrating between them is relatively straightforward due to API similarities, allowing teams to switch as needs evolve.
For the most up-to-date guidance, refer to the official Playwright documentation and Puppeteer documentation directly. Download trends can be tracked at npm trends.
Official Resources
- Playwright Documentation
- Puppeteer Documentation
- npm trends — Puppeteer vs Playwright
- W3C WebDriver Specification
See Also
- Katalon Studio: Complete All-in-One Test Automation Platform
- Katalon Studio: Complete All-in-One Test Automation Platform
- Robot Framework: Mastering Keyword-Driven Test Automation - Comprehensive guide to Robot Framework’s keyword-driven approach for test…
- Robot Framework: Mastering Keyword-Driven Test Automation - Comprehensive guide to Robot Framework’s keyword-driven approach…
- Testim & Mabl: AI-Powered Self-Healing Test Automation Platforms - Complete guide to Testim and Mabl self-healing test automation…
- Postman Alternatives 2025: Bruno vs Insomnia vs Thunder Client Comparison - Comprehensive comparison of Postman alternatives including Bruno,…
