Skip to content
FLAVIO COPES
flaviocopes.com
2026

Introduction to Puppeteer

By

Learn how to automate Chrome from Node.js with Puppeteer, including navigation, locators, page evaluation, screenshots, PDFs, and device emulation.

~~~

Puppeteer is a JavaScript library for controlling Chrome or Firefox programmatically.

You can use it to:

Puppeteer runs headless by default, which means the browser has no visible window. You can also run it with a normal browser window while developing and debugging.

The current API is documented at pptr.dev.

Install Puppeteer

Create a project and install the package:

mkdir puppeteer-demo
cd puppeteer-demo
npm init -y
npm install puppeteer

The puppeteer package downloads compatible Chrome for Testing and chrome-headless-shell binaries during installation. The download is large because it includes real browser executables.

Some package managers can block dependency install scripts. If Puppeteer reports that it cannot find Chrome, run:

npx puppeteer browsers install

See the official installation guide for browser-cache and download configuration.

puppeteer or puppeteer-core?

Use puppeteer when you want the browser download and convenient defaults.

Use puppeteer-core when you manage the browser executable yourself or connect to a remote browser. It does not download Chrome and does not assume an executable path.

When launching a local browser with puppeteer-core, provide an executablePath or a supported installed-browser channel.

Launch a browser

Create a file named index.mjs:

import puppeteer from 'puppeteer'

const browser = await puppeteer.launch()

try {
  const page = await browser.newPage()

  await page.goto('https://example.com/', {
    waitUntil: 'domcontentloaded'
  })

  console.log(await page.title())

  await page.screenshot({
    path: 'example.png',
    fullPage: true
  })
} finally {
  await browser.close()
}

Run it:

node index.mjs

Always close the browser. A try/finally block makes sure that happens even when navigation or another operation fails.

Show the browser while debugging

Pass headless: false to launch():

const browser = await puppeteer.launch({
  headless: false,
  slowMo: 100
})

slowMo adds a delay between Puppeteer operations, which makes the automation easier to watch.

Work with pages

browser.newPage() creates a new tab and returns a Page object.

Common methods include:

Most Puppeteer operations are asynchronous and return promises, so you normally use await.

Find and interact with elements

For user interactions, prefer locators:

await page.locator('input[name="email"]').fill('[email protected]')
await page.locator('button[type="submit"]').click()

Locators wait for an element to be ready and retry the action when the relevant conditions are not met yet.

You can also use CSS selectors with direct page methods:

await page.click('button[type="submit"]')
await page.type('input[name="email"]', '[email protected]')
await page.select('select[name="country"]', 'it')
await page.hover('.menu')
await page.focus('input[name="email"]')

The $() method returns the first matching element handle or null. The $$() method returns all matching element handles:

const firstLink = await page.$('a')
const allLinks = await page.$$('a')

Dispose handles when you keep them around:

await firstLink?.dispose()

for (const link of allLinks) {
  await link.dispose()
}

Read data from the page

page.$eval() finds the first matching element and passes it to a function that runs in the page:

const heading = await page.$eval('h1', element => element.textContent)

page.$$eval() passes an array of all matching elements:

const links = await page.$$eval('a', elements =>
  elements.map(element => element.href)
)

Use page.evaluate() when you need to run arbitrary browser-side code:

const result = await page.evaluate(() => {
  return {
    title: document.title,
    paragraphs: document.querySelectorAll('p').length
  }
})

The function runs inside the page, not inside Node.js. It cannot directly access variables, modules, or filesystem APIs from the Node.js process.

Pass values as additional arguments when the page function needs Node-side data:

const selector = 'main article'

const count = await page.evaluate(value => {
  return document.querySelectorAll(value).length
}, selector)

Values returned by evaluate() must be serializable. Use evaluateHandle() when you intentionally need a handle to an in-page object, then dispose that handle when finished.

Expose a Node.js function to the page

page.exposeFunction() adds a function to the page that runs in the Node.js context:

import { readFile } from 'node:fs/promises'

await page.exposeFunction('loadTextFile', async path => {
  return readFile(path, 'utf8')
})

const contents = await page.evaluate(() => {
  return window.loadTextFile('notes.txt')
})

Do not expose unrestricted filesystem or network access to untrusted page code.

Wait for the right condition

Do not add arbitrary delays when you can wait for the state you need.

Wait for an element:

await page.waitForSelector('.results', { visible: true })

Wait for a browser-side condition:

await page.waitForFunction(() => {
  return document.querySelectorAll('.result').length > 0
})

Wait for a response:

const responsePromise = page.waitForResponse(response =>
  response.url().includes('/api/search') && response.ok()
)

await page.locator('button[type="submit"]').click()
const response = await responsePromise

Older Puppeteer tutorials often use page.waitFor(), page.waitForTimeout(), or page.waitForXPath(). Those methods are not part of the current API. Use the specific wait method you need. Current selectors can also use Puppeteer’s selector syntax for text, accessibility, and XPath queries.

Avoid navigation race conditions

If an action triggers a navigation, start waiting before the action:

await Promise.all([
  page.waitForNavigation({ waitUntil: 'domcontentloaded' }),
  page.locator('a.checkout').click()
])

Starting waitForNavigation() after the click can miss a fast navigation.

Emulate a device

Puppeteer exports a current set of device profiles as KnownDevices:

import puppeteer, { KnownDevices } from 'puppeteer'

const browser = await puppeteer.launch()

try {
  const page = await browser.newPage()
  await page.emulate(KnownDevices['iPhone 15 Pro'])
  await page.goto('https://example.com/')
  await page.screenshot({ path: 'mobile.png' })
} finally {
  await browser.close()
}

Device emulation changes settings such as the viewport, user agent, and touch support. It is useful, but it is not a replacement for testing on a real device.

Change the viewport

Use setViewport():

await page.setViewport({
  width: 1280,
  height: 800,
  deviceScaleFactor: 1
})

The method is named setViewport(), not setViewPort().

Create a PDF

page.pdf() renders the page using print CSS by default:

await page.pdf({
  path: 'page.pdf',
  format: 'A4',
  printBackground: true
})

To render with screen media rules:

await page.emulateMediaType('screen')
await page.pdf({ path: 'page.pdf' })

A note about scraping

Browser automation does not give you permission to collect any data you want.

Respect the site’s terms, authentication boundaries, rate limits, robots policies where applicable, copyright, privacy, and the law. Keep request rates low and prefer an official API when one exists.

The complete and current method list is in the official Page API.

Tagged: Node.js · All topics
~~~

Related posts about node: