# Puppeteer tutorial: first script, scraping, downloading images and Firefox

> A practical Puppeteer tutorial, run on Puppeteer 25.7: install, first script, reading and clicking, downloading images, and what works in Firefox.

Published 2026-11-07 · https://shareglint.com/en/guides/puppeteer-tutorial

Puppeteer is a Node.js library that drives a real browser from code: it opens pages, clicks and types, reads what is on the page, and takes screenshots and PDFs. This tutorial goes from installation to a working script, then covers two things people search for most: **downloading images** from a page and running Puppeteer with **Firefox**. Every script here was run on 2026-11-07 with Puppeteer 25.7.0, Chrome 152.0.7977.42 and Firefox 153.0.4, in the official `ghcr.io/puppeteer/puppeteer` Docker image, against a small local test shop page; the numbers below come from those runs.

## Install

```bash
npm i puppeteer
```

Installing `puppeteer` also downloads a browser: Chrome for Testing and `chrome-headless-shell`, into `~/.cache/puppeteer` ([installation docs](https://pptr.dev/guides/installation)). In the official image they took **391 MB and 261 MB** on disk. Many package managers now block install scripts by default; then the download is skipped and the first launch fails with "Could not find Chrome". Run `npx puppeteer browsers install`, or allow Puppeteer's install script.

If you bring your own browser or connect to a remote one, install `puppeteer-core` instead: the same API, no download.

## A first script

```javascript
import puppeteer from 'puppeteer';

const browser = await puppeteer.launch();
try {
  const page = await browser.newPage();
  await page.setViewport({ width: 1280, height: 800 });
  const response = await page.goto('https://example.com/', { waitUntil: 'networkidle2' });
  console.log(response.status(), await page.title());
  await page.screenshot({ path: 'page.png' });
} finally {
  await browser.close();
}
```

Save it as `first.mjs` and run `node first.mjs`. Three habits from the start:

- **Close the browser in `finally`.** A script that throws before `close()` leaves a Chrome process running; in a server or a loop they add up.
- **Choose `waitUntil` deliberately.** `networkidle2` waits until at most two requests are open for 500 ms. `networkidle0` never resolves on a page that keeps a connection open — in our [speed tests](https://shareglint.com/en/guides/speed-up-puppeteer-screenshots) it timed out.
- **Set the viewport.** The default is 800×600, which puts many sites in their tablet layout.

## Reading the page

Most scraping is `$$eval`: select elements and map them to plain data inside the page.

```javascript
const products = await page.$$eval('.product', (els) =>
  els.map((el) => ({
    name: el.querySelector('.name').textContent.trim(),
    price: el.querySelector('.price').textContent.trim(),
  })),
);
```

On our test page this returned all five products, the first as `{"name":"Item 1","price":"10.00"}`. The function runs **in the browser**, so it cannot use your Node variables; pass values as extra arguments.

Content that arrives later — after a script, a request or a click — needs a wait. `await page.waitForSelector('#late')` returned as soon as our page added the element half a second after load, which is better than a fixed `setTimeout`.

## Clicking and typing

Locators find an element, wait until it can be used, then act:

```javascript
await page.locator('input[name=q]').fill('lamp');
await Promise.all([
  page.waitForNavigation(),
  page.locator('form button').click(),
]);
console.log(await page.$eval('h1', (el) => el.textContent));
```

Start waiting for the navigation **before** the click, in the same `Promise.all`; started after it, the navigation may already be over and the wait hangs. Our run landed on `/search?q=lamp` with "Results for lamp". The [page interactions guide](https://pptr.dev/guides/page-interactions) covers locators in full.

## Screenshots and PDFs

`page.screenshot()` and `page.pdf()` are one line each; the details fill their own guides: [screenshots](https://shareglint.com/en/guides/puppeteer-screenshot) (viewport, element, retina), [full-page screenshots](https://shareglint.com/en/guides/puppeteer-full-page-screenshot) (lazy images, size limits), [PDFs](https://shareglint.com/en/guides/puppeteer-pdf) (page size, headers, page breaks) and [emoji and fonts](https://shareglint.com/en/guides/puppeteer-emoji-and-fonts).

## Downloading images from a page

There are two good ways, and each misses something. Our test page had five product images, one CSS background image and one lazy image far below the fold.

**1. Save image responses** while the page loads. You get the exact bytes the browser received, with the page's cookies:

```javascript
import fs from 'node:fs';
import path from 'node:path';

fs.mkdirSync('images', { recursive: true });
const saved = [];
page.on('response', (res) => {
  const type = res.headers()['content-type'] || '';
  if (!type.startsWith('image/')) return;
  saved.push(res.buffer().then((buf) => {
    const name = path.basename(new URL(res.url()).pathname) || 'image';
    fs.writeFileSync(path.join('images', name), buf);
  }).catch(() => {}));
});
await page.goto(url, { waitUntil: 'networkidle0' });
await Promise.all(saved);
```

In Chrome this caught **6 images**: the five products and the CSS background. It missed the lazy image 3,000 px below, which Chrome never requested; in the same test Firefox did load it and saved all 7 — browsers decide differently when a lazy image is close enough to load. Do not rely on either: scroll the page or set lazy images to eager first, as in the [full-page guide](https://shareglint.com/en/guides/puppeteer-full-page-screenshot). Filter by `content-type`, not `request.resourceType()` — the latter does not exist in Firefox (below).

**2. Collect the image URLs** and download them yourself:

```javascript
const urls = await page.$$eval('img', (imgs) => imgs.map((img) => img.currentSrc || img.src));
for (const url of urls) {
  const res = await fetch(url);
  fs.writeFileSync(path.join('images', path.basename(new URL(url).pathname)), Buffer.from(await res.arrayBuffer()));
}
```

This found **6 URLs** — the five products and the lazy image — but not the CSS background, which is not an `<img>`. Node's `fetch` does not send the page's cookies, so images behind a login fail; the first method does not have that problem. Use `currentSrc` so a `srcset` gives the file the browser chose.

For a picture of an element as it looks on the page (cropped, scaled, with CSS applied), take an element screenshot instead: `await (await page.$('img.hero')).screenshot({ path: 'hero.png' })`.

Respect the site's terms and copyright: being able to download an image does not mean you may reuse it.

## Puppeteer with Firefox

Puppeteer drives Firefox over [WebDriver BiDi](https://pptr.dev/webdriver-bidi), the cross-browser automation protocol:

```bash
npx puppeteer browsers install firefox
```

```javascript
const browser = await puppeteer.launch({ browser: 'firefox' });
```

Firefox took **310 MB** in the cache. In the official Puppeteer image it would not start at first ("libX11-xcb.so.1: cannot open shared object file"); after installing `libx11-xcb1`, `libgtk-3-0` and `libdbus-glib-1-2` it ran. Puppeteer's own docs list the features that are not supported over WebDriver BiDi; they throw `UnsupportedOperation`. Our test of common calls:

| Call | Chrome 152 | Firefox 153 |
|---|---|---|
| `goto`, `$$eval`, `waitForSelector`, locators | works | works |
| `screenshot` (PNG, WebP, full page, element) | works | works |
| `pdf` | works | works |
| `setRequestInterception` (blocked 6 images) | works | works |
| `response.buffer()` | works | works |
| `setCookie` / `cookies`, `setUserAgent` | works | works |
| `request.resourceType()` | works | **UnsupportedOperation** |
| `emulateMediaFeatures` (dark mode) | works | **UnsupportedOperation** |
| `page.metrics()`, `createCDPSession()` | works | **UnsupportedOperation** |

Launch, load and screenshot took about **1.7 s** with Firefox against 0.4 s with Chrome on the same page. An `UnsupportedOperation` thrown inside an event handler (a `request` listener calling `resourceType()`) crashes Node with an uncaught exception, so test scripts in Firefox before relying on them. Use Firefox when you need to see the page as Firefox renders it; for everything else Chrome is the faster default.

## Headless modes and the user agent

`puppeteer.launch()` runs Chrome's headless mode. `{ headless: 'shell' }` runs the separate `chrome-headless-shell`, which the [headless modes guide](https://pptr.dev/guides/headless-modes) calls more performant but not identical to Chrome; in our run it started about 0.1 s faster. `{ headless: false }` opens a visible window for debugging.

Both headless modes announce themselves: the user agent contained **`HeadlessChrome/152`**, and some sites block or change pages for it. `page.setUserAgent()` changes it if you have a reason to.

## Common mistakes

- **Not awaiting.** Almost every Puppeteer call returns a promise; a missing `await` acts on a page that is not ready.
- **Using Node variables inside `$$eval` or `evaluate`.** The function runs in the page; pass arguments.
- **One browser per request in a server.** Launching costs hundreds of milliseconds; reuse one browser and open pages ([speed guide](https://shareglint.com/en/guides/speed-up-puppeteer-screenshots)).
- **Missing fonts in Docker.** Text and emoji turn into boxes ([emoji and fonts](https://shareglint.com/en/guides/puppeteer-emoji-and-fonts)).

## When not to run Puppeteer yourself

If what you need from Puppeteer is a screenshot of a public page or an image made from HTML, running Chrome yourself means servers, fonts, memory and timeouts to look after. Our [screenshot API](https://shareglint.com/en/screenshot-api) takes screenshots of public URLs in one request — full page, devices, cookie banners hidden — and [templates](https://shareglint.com/en/templates) turn HTML and your data into images and PDFs. For scraping, form filling, tests and pages behind your login, Puppeteer on your own machine is the right tool.

## FAQ

### What is Puppeteer?

A Node.js library from the Chrome team that drives a real browser from code: open pages, click, type, read the page, take screenshots and PDFs. It downloads its own Chrome for Testing when installed and also drives Firefox.

### Does Puppeteer work with Firefox?

Yes: install Firefox with npx puppeteer browsers install firefox and launch with { browser: 'firefox' }. It runs over WebDriver BiDi, so CDP-only features throw UnsupportedOperation — in our test request.resourceType(), emulateMediaFeatures(), page.metrics() and createCDPSession(). Screenshots, PDFs, cookies and request interception worked.

### How do I download all images from a page with Puppeteer?

Listen to page responses and save the bodies of image responses, or collect the img elements' currentSrc and download each URL. Responses also catch CSS background images; a src list also finds lazy images that never loaded. Use both if you need everything.

### Why does my Puppeteer install not download Chrome?

Your package manager blocked Puppeteer's install script, which many now do by default. Run npx puppeteer browsers install, or allow Puppeteer's install script in your package manager's settings.
