Tutorial · Node.js

Puppeteer screenshots: full page, elements and mobile

Puppeteer drives Chrome from Node.js. Here are the screenshot recipes you need, tested with Puppeteer 25, and the problems you will run into once screenshots run on a server.

Updated October 29, 2026 · 8 min read

To take a Puppeteer screenshot, launch a browser, open the page and call page.screenshot({ path: "page.png" }). Add fullPage: true for the whole scrollable page, or call screenshot() on an element handle for one element. Puppeteer is a JavaScript library that controls Chrome or Firefox over the DevTools Protocol or WebDriver BiDi.

The examples below were run with Puppeteer 25.16 and Chrome 156 on Node.js 26, against example.com and a local test page with lazy-loaded images, a cookie banner and a chat widget.

Install Puppeteer

Puppeteer 25 needs Node.js 22.12 or later.

npm install puppeteer
npx puppeteer browsers install   # downloads Chrome for Testing if the install didn't

npm install puppeteer used to download a matching Chrome in an install script. Newer npm versions, pnpm, Yarn Berry and Bun block dependency install scripts by default, so the browser can be missing and puppeteer.launch() fails. The second command installs it either way, into ~/.cache/puppeteer. Already have Chrome? Use puppeteer-core and pass executablePath to launch().

1. Take your first screenshot

// screenshot.mjs
import puppeteer from "puppeteer";

const browser = await puppeteer.launch();
try {
  const page = await browser.newPage();
  await page.setViewport({ width: 1280, height: 800 });
  await page.goto("https://example.com", { waitUntil: "networkidle2" });
  await page.screenshot({ path: "example.png" });
  await page.screenshot({ path: "example-full.png", fullPage: true });
} finally {
  await browser.close();
}

The default viewport is 800 × 600, so set one. waitUntil decides when goto returns:

waitUntilReturns when
"load" (default)The load event fired: HTML, styles and images loaded
"domcontentloaded"The HTML is parsed; images may still be missing
"networkidle2"No more than 2 network connections for 500 ms; good for pages that keep a connection open
"networkidle0"No network connections for 500 ms

try/finally matters as soon as this runs in a loop or a server: a page that throws must not leave a Chrome process behind.

2. Full-page screenshots with lazy-loaded images

fullPage: true captures the page as it is at that moment. On our test page, a plain full-page screenshot showed 3 of 5 native lazy images (loading="lazy"), none of the 5 images that load by script when scrolled into view, and the fixed cookie banner in the middle of the image. This version scrolls first, waits for images and fonts, and hides the banner and the chat bubble:

// full-page.mjs
import puppeteer from "puppeteer";

const browser = await puppeteer.launch();
try {
  const page = await browser.newPage();
  await page.setViewport({ width: 1280, height: 800 });
  await page.goto("https://example.com", { waitUntil: "networkidle2" });

  // Scroll one screen at a time so lazy images start loading, then back to the top
  await page.evaluate(async () => {
    for (let y = 0; y < document.body.scrollHeight; y += window.innerHeight) {
      window.scrollTo(0, y);
      await new Promise((resolve) => setTimeout(resolve, 100));
    }
    window.scrollTo(0, 0);
  });
  await page.waitForFunction(() => [...document.images].every((img) => img.complete));
  await page.evaluate(() => document.fonts.ready); // web fonts, not fallback fonts

  // Hide what should not be in the picture
  await page.addStyleTag({
    content: "#cookie-banner, .chat-launcher { display: none !important; }",
  });

  await page.screenshot({ path: "full.png", fullPage: true });
} finally {
  await browser.close();
}

With this script, all ten images loaded and the banner was gone. The selectors are those of our test page; every site names its cookie banner and chat widget differently, and the banner can also differ by country.

3. Formats, regions and retina

await page.screenshot({ path: "page.jpg", type: "jpeg", quality: 80 });
await page.screenshot({ path: "page.webp", type: "webp", quality: 80 });
await page.screenshot({ path: "top.png", clip: { x: 0, y: 0, width: 1280, height: 400 } });
await page.screenshot({ path: "transparent.png", omitBackground: true });
const png = await page.screenshot(); // a Uint8Array, for uploading instead of saving
const base64 = await page.screenshot({ encoding: "base64" }); // for a data: URL or JSON
OptionWhat it does
pathFile to write; the extension sets the format if type is missing
type, quality"png" (default), "jpeg" or "webp"; quality 0–100 for JPEG and WebP
fullPageThe whole scrollable page
clipA region { x, y, width, height }
omitBackgroundTransparent where the page has no background of its own
encoding"binary" (default, a Uint8Array) or "base64"
optimizeForSpeedFaster encoding, larger files

For a retina screenshot, raise the device scale factor; the layout stays the same and the image gets twice as many pixels in each direction:

await page.setViewport({ width: 1280, height: 800, deviceScaleFactor: 2 });
await page.screenshot({ path: "retina.png" }); // 2560 × 1600 pixels

4. Screenshot one element

const pricing = await page.waitForSelector("#pricing");
await pricing.screenshot({ path: "pricing.png" });

waitForSelector waits until the element exists; screenshot() on the handle scrolls it into view and crops to its box. With deviceScaleFactor: 2 still set from the previous step, our 1280 × 581 pricing section came out at 2560 × 1162 pixels.

5. Mobile devices and dark mode

KnownDevices holds the viewport, device scale factor, touch support and user agent of common phones and tablets:

// mobile.mjs
import puppeteer, { KnownDevices } from "puppeteer";

const browser = await puppeteer.launch();
try {
  const page = await browser.newPage();
  await page.emulate(KnownDevices["iPhone 15 Pro"]); // viewport, scale factor, touch, user agent
  await page.goto("https://example.com", { waitUntil: "networkidle2" });
  await page.screenshot({ path: "iphone-15-pro.png" });

  await page.emulateMediaFeatures([{ name: "prefers-color-scheme", value: "dark" }]);
  await page.screenshot({ path: "iphone-15-pro-dark.png" });
} finally {
  await browser.close();
}

The iPhone 15 Pro screenshot is 1179 × 1977 pixels: a 393 × 659 viewport at scale 3. It’s still Chrome’s engine with an iPhone’s size and user agent, not Safari; for WebKit rendering, use Playwright. The dimensions of common devices are on our viewport sizes pages.

Set the color scheme explicitly in both directions. Headless Chrome follows the operating system: on our test Mac, which runs in dark mode, every screenshot without emulateMediaFeatures came out dark. { name: "prefers-color-scheme", value: "light" } pins it to light.

6. Run Puppeteer in production

A script on your laptop is one thing; screenshots as part of a service are another. What breaks first:

ProblemWhat happensWhat helps
MemoryEach page needs tens to hundreds of MB; pages that are never closed add up until the server swaps or the process is killedClose every page in finally, limit open pages, restart the browser now and then
Crashes and hangsChrome can crash or a page can hang, and the next request fails tooTimeouts on goto, a health check, browser.on("disconnected") to relaunch
Fonts and emojiLinux servers lack fonts, so pages render with fallback fonts and empty boxesInstall font packages (e.g. Noto and Noto Color Emoji) in your image
Bot detectionThe default user agent says HeadlessChrome; many sites block or challenge itSet a regular user agent; some sites still block data-center IPs
Cookie banners and adsDifferent per site and per countryPer-site CSS or clicks, maintained by you
Untrusted URLsUsers can point your service at internal addressesAllow only public URLs; block private networks

A minimal service that reuses one browser, opens a page per request and caps the open pages:

// screenshot-service.mjs — one browser, a new page per request, at most 4 at a time
import http from "node:http";
import puppeteer from "puppeteer";

const browser = await puppeteer.launch();
const MAX_PAGES = 4;
let open = 0;

async function screenshot(url) {
  const page = await browser.newPage();
  try {
    await page.setViewport({ width: 1280, height: 800 });
    await page.goto(url, { waitUntil: "networkidle2", timeout: 30_000 });
    return await page.screenshot({ type: "png" });
  } finally {
    await page.close(); // always, or every failed page keeps its memory
  }
}

http
  .createServer(async (req, res) => {
    const target = new URL(req.url, "http://localhost").searchParams.get("url");
    if (!target?.startsWith("https://")) return res.writeHead(400).end("url must be https://");
    if (open >= MAX_PAGES) return res.writeHead(503, { "Retry-After": "1" }).end("busy");
    open++;
    try {
      const png = await screenshot(target);
      res.writeHead(200, { "Content-Type": "image/png" }).end(png);
    } catch (err) {
      res.writeHead(502).end(String(err.message));
    } finally {
      open--;
    }
  })
  .listen(8080);

In our test, six parallel requests got four screenshots and two 503 answers, and an unreachable host returned a 502 with Chrome’s network error. The https:// check is not enough protection on its own: before you expose such a service, also refuse host names that resolve to private or internal addresses.

7. Or call a screenshot API instead

A screenshot API runs the browsers, fonts, queues and banner blocking for you. With screenshotbase, the full-page screenshot from section 2 is one request:

curl -G https://api.screenshotbase.com/v1/take \
  -H "apikey: $SCREENSHOTBASE_KEY" \
  --data-urlencode "url=https://example.com" \
  -d full_page=1 \
  -d dark_mode=1 \
  -d block_cookie_banners=1 \
  -o example.png

The same from Node.js, without Chrome on your side:

// api-screenshot.mjs — no browser on your side
import { writeFile } from "node:fs/promises";

const params = new URLSearchParams({
  url: "https://example.com",
  full_page: "1", // scrolls first, so lazy images load
  viewport_width: "1280",
  block_cookie_banners: "1",
  format: "webp",
});
const res = await fetch(`https://api.screenshotbase.com/v1/take?${params}`, {
  headers: { apikey: process.env.SCREENSHOTBASE_KEY },
  signal: AbortSignal.timeout(120_000), // a render may take up to 110 seconds
});
if (!res.ok) {
  const body = await res.json().catch(() => ({}));
  throw new Error(`screenshotbase ${res.status} ${body.error?.code}: ${body.message}`);
}
await writeFile("example.webp", Buffer.from(await res.arrayBuffer()));
Puppeteerscreenshotbase /v1/take
page.screenshot({ fullPage: true })full_page=1, which scrolls first and draws bottom bars at the end
page.setViewport({ width, height, deviceScaleFactor })viewport_width, viewport_height, device_scale_factor
page.emulate(KnownDevices["iPhone 15 Pro"])viewport_device=iphone-15-pro
emulateMediaFeatures (dark)dark_mode=1
elementHandle.screenshot()selector=#pricing
clip, type, quality, omitBackgroundclip_*, format, quality, omit_background
addStyleTagstyles, hide_selectors, block_cookie_banners, block_chats, block_ads
waitUntil, waitForSelectorwait_until, wait_for_selector, delay
setExtraHTTPHeaders, browser.setCookie, authenticateheaders, cookies, authorization
--proxy-serverproxy, or ip_country_code for one of 117 countries
page.pdf()format=pdf
page.click, page.type, page.evaluateactions (up to 20 steps) and scripts

The API also returns AVIF, SVG and markdown, renders HTML or markdown you send, and runs long renders as async jobs with a webhook. It doesn’t solve CAPTCHAs: a bot challenge page is captured as it is, or fails the request with ignore_host_errors=0. For tests and long multi-step flows behind a login, keep Puppeteer.

The free plan has 300 screenshots a month for testing and personal projects; paid plans start at $15 a month for 3,000 (pricing). The Puppeteer screenshot API alternative page puts both side by side with sources. Try it without code with the website screenshot tool or the mobile screenshot tool.

Frequently asked questions

How do I take a full-page screenshot with Puppeteer?

Call await page.screenshot({ path: 'page.png', fullPage: true }) after the page has loaded. If the page lazy-loads images, scroll through it first and wait for the images, otherwise they stay empty.

Why are images missing in my Puppeteer screenshot?

They are lazy-loaded and only start loading when they scroll into view. On our test page, a plain full-page screenshot showed 3 of 5 native lazy images and none of the 5 images loaded by script. Scroll through the page with page.evaluate and wait until every image is complete.

Why does npm install puppeteer not download Chrome?

Newer npm versions, pnpm, Yarn Berry and Bun block install scripts of dependencies by default, and Puppeteer downloads Chrome in one. Run npx puppeteer browsers install after the install, or allow the script by adding "puppeteer": true to allowScripts in package.json.

How do I take a screenshot of one element with Puppeteer?

Wait for it and screenshot the handle: const el = await page.waitForSelector('#pricing'); await el.screenshot({ path: 'pricing.png' });

Puppeteer or Playwright for screenshots?

Both drive Chrome with nearly the same screenshot options. Puppeteer is JavaScript only and focused on Chrome (with Firefox support); Playwright adds Python, Java and .NET, WebKit, device profiles per context and a test runner with screenshot comparison. See our Playwright screenshot guide.

Screenshot API

Take screenshots from your code

screenshotbase renders any website in a real browser and returns the image: full page or viewport, any device size, retina, from 117 countries (paid plans), with cookie banners and ads hidden.

/v1/take documentation Screenshot API All documentation

300 free screenshots every month. No credit card required.

GET https://api.screenshotbase.com/v1/take?url=https://example.com&full_page=true

# Response: the image (PNG, JPG, WebP, AVIF or GIF)
curl -G "https://api.screenshotbase.com/v1/take" \
  --data-urlencode "url=https://example.com" \
  -d viewport_width=1280 -d viewport_height=800 \
  -d full_page=true -d format=png \
  -H "apikey: YOUR-API-KEY" \
  -o screenshot.png

Keep reading