Introduction to Puppeteer
By Flavio Copes
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:
- automate browser interactions
- test websites in a real browser
- scrape web pages
- take screenshots
- generate PDFs
- inspect page content
- measure and trace loading behavior
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:
page.goto()to navigatepage.reload()to reloadpage.goBack()andpage.goForward()for history navigationpage.title()to read the document titlepage.url()to read the current URLpage.content()to get the serialized HTMLpage.setContent()to replace the page HTMLpage.screenshot()to capture an imagepage.pdf()to create a PDF
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.
Related posts about node: