puppeteer
Puppeteer is a Node.js library that drives Chrome or Firefox through the DevTools Protocol or WebDriver BiDi, headless by default. You script a real browser: open pages, click, type, wait for selectors, run JavaScript inside the page, capture screenshots and PDFs, and intercept network requests. Installing the puppeteer package also downloads a matching Chrome build so the API and browser version stay in sync (puppeteer-core skips the download). It is maintained by the Chrome DevTools team, which is why new Chrome capabilities land here first.
Still the cleanest way to script Chrome specifically, and the Chrome team keeps it current with every browser release. For cross-browser test suites, start with Playwright instead and keep Puppeteer for Chrome-centric automation.
Use it if
- You need programmatic Chrome for scraping, screenshots, or PDF generation from HTML
- You are automating something Chrome-specific: extensions, DevTools Protocol features, performance traces
- You want the Chrome-team-maintained way to drive the browser most of your users actually run
- You run crawling infrastructure where a pinned, auto-downloaded Chrome per package version keeps CI reproducible
- You are writing cross-browser end-to-end tests; Playwright covers Chromium, Firefox, and WebKit with a built-in test runner, while Puppeteer has no runner and Firefox support rides on WebDriver BiDi with caveats
- You want retries, fixtures, parallel workers, and trace viewing out of the box; Puppeteer is a browser driver, not a test framework, so you assemble the Jest or Mocha plumbing yourself
- You are on constrained CI or serverless images: every default install pulls a full Chrome build (hundreds of MB) unless you switch to puppeteer-core and manage the browser yourself
- You need Safari automation; it is not supported at all
Setup reality
npm i puppeteer downloads a compatible Chrome during install, except that npm, pnpm, Yarn, Bun, and Deno now block install scripts by default, so the browser silently never arrives and you get a could-not-find-Chrome error at runtime. The fix is npx puppeteer browsers install or allow-listing the script in package.json. Node 22.12+ is required as of v24. In Docker and CI you additionally need a stack of system libraries and usually --no-sandbox and --disable-dev-shm-usage flags; the troubleshooting page is required reading before containerizing.
Patterns
Launch a browser and open a pagelaunch-and-navigate
import puppeteer from 'puppeteer';
const browser = await puppeteer.launch();
const page = await browser.newPage();
await page.goto('https://example.com', { waitUntil: 'networkidle2' });
// ...
await browser.close();launch() is headless by default; pass { headless: false } to watch it work.
Take a screenshotscreenshot
await page.setViewport({ width: 1280, height: 800 });
await page.screenshot({ path: 'page.png', fullPage: true });The default viewport is 800x600; set it explicitly or layouts render narrower than you expect.
Render a page to PDFgenerate-pdf
await page.goto('https://example.com', { waitUntil: 'networkidle2' });
await page.pdf({ path: 'page.pdf', format: 'A4', printBackground: true });printBackground defaults to false, which strips CSS backgrounds and surprises everyone the first time.
Fill a form with locatorsclick-and-type
await page.locator('input[name="q"]').fill('puppeteer');
await page.locator('button[type="submit"]').click();Locators auto-wait for the element to be visible and stable; prefer them over the older page.click and page.type.
Wait for content by text or ARIA rolewait-for-element
const handle = await page
.locator('::-p-text(Search results)')
.waitHandle();
const text = await handle.evaluate((el) => el.textContent);::-p-text and ::-p-aria are Puppeteer selector extensions, handy when CSS selectors are brittle.
Run JavaScript inside the pageevaluate-in-page
const title = await page.evaluate(() => document.title);
const sum = await page.evaluate((a, b) => a + b, 2, 3);The callback executes in the browser and cannot close over Node variables; pass them as arguments.
Extract data from many elementsscrape-list
const links = await page.$$eval('a.result', (anchors) =>
anchors.map((a) => ({ href: a.href, text: a.textContent })),
);The return value must be JSON-serializable; DOM nodes cannot cross the browser-to-Node boundary.
Block or modify network requestsintercept-requests
await page.setRequestInterception(true);
page.on('request', (req) => {
if (['image', 'font'].includes(req.resourceType())) req.abort();
else req.continue();
});Once interception is on, every request must be continued or aborted exactly once or the page hangs.
Save and restore a login sessionreuse-cookies
import fs from 'node:fs';
// after logging in once
const cookies = await browser.cookies();
fs.writeFileSync('cookies.json', JSON.stringify(cookies));
// in a later session
await browser.setCookie(...JSON.parse(fs.readFileSync('cookies.json', 'utf8')));page.cookies() is deprecated; the cookie APIs now live on the Browser object.
Launch inside Docker or CIdocker-ci-flags
const browser = await puppeteer.launch({
args: ['--no-sandbox', '--disable-dev-shm-usage'],
});Most containers need both flags; the tiny default /dev/shm otherwise crashes Chrome on heavier pages.
Drive an existing Chrome with puppeteer-coreuse-system-chrome
import puppeteer from 'puppeteer-core';
const browser = await puppeteer.launch({
executablePath: '/usr/bin/google-chrome',
});puppeteer-core skips the browser download entirely, but version compatibility becomes your problem.
Install Chrome when the postinstall was blockedinstall-browser
npx puppeteer browsers install chromeModern package managers block install scripts by default, so this step is now routine in CI.
Alternatives
| Package | Registry | Pick it when |
|---|---|---|
| playwright | npm | You need cross-browser coverage or an integrated test runner with fixtures and traces |
| selenium-webdriver | npm | You must target the WebDriver standard across many browsers or an existing Selenium grid |
| cypress | npm | You want an all-in-one interactive testing experience for your own web app rather than a scripting driver |