· Tutorials

Taking Screenshots with Puppeteer: A Practical Guide

Learn how to capture web page screenshots with Puppeteer and headless Chrome in Node.js — from basic usage to full-page captures, viewports, and production pitfalls.

Laptop screen showing code in a terminal

Puppeteer is Google's Node.js library for controlling headless Chrome. It's one of the most popular tools for taking screenshots of web pages programmatically, and for good reason — it gives you full control over a real browser.

In this guide, we'll walk through how to capture screenshots with Puppeteer, from the basics to more advanced techniques, and cover some of the challenges you'll hit when running it in production.

Getting Started

Install Puppeteer in your Node.js project. It downloads a bundled version of Chromium automatically:

npm install puppeteer

If you want to use an existing Chrome installation instead, install puppeteer-core and provide the executable path yourself.

Here's the simplest possible screenshot:

const puppeteer = require('puppeteer');

(async () => {
  const browser = await puppeteer.launch();
  const page = await browser.newPage();
  await page.goto('https://example.com');
  await page.screenshot({ path: 'screenshot.png' });
  await browser.close();
})();

That's it — five lines to capture a web page as a PNG. Puppeteer launches a headless Chrome instance, navigates to the URL, saves the screenshot, and shuts down.

Setting the Viewport Size

By default, Puppeteer uses an 800x600 viewport. For most real-world uses, you'll want to change this. Set the viewport before navigating:

await page.setViewport({ width: 1280, height: 720 });
await page.goto('https://example.com');
await page.screenshot({ path: 'screenshot.png' });

For high-DPI screenshots (like Retina displays), set the deviceScaleFactor:

await page.setViewport({
  width: 1280,
  height: 720,
  deviceScaleFactor: 2
});

This produces a 2560x1440 image — useful when you need crisp screenshots for documentation or marketing pages.

Full-Page Screenshots

By default, Puppeteer only captures the visible viewport. To capture the entire scrollable page, set fullPage to true:

await page.screenshot({
  path: 'full-page.png',
  fullPage: true
});

Keep in mind that full-page screenshots of long pages can produce very large images. A page with a lot of content might generate a PNG that's several megabytes.

For a deeper look at handling lazy-loaded content, infinite scroll, sticky headers, and memory optimization, see our complete guide to Puppeteer full-page screenshots.

Capturing a Specific Element

Sometimes you only need a screenshot of a particular section — a chart, a card, or a hero banner. Puppeteer lets you screenshot a single element:

const element = await page.$('.pricing-table');
await element.screenshot({ path: 'pricing.png' });

This clips the screenshot to the bounding box of that element, which is handy for generating images of specific components.

Waiting for Content to Load

One of the trickiest parts of taking screenshots is making sure the page is actually ready. Dynamic content, lazy-loaded images, and client-side rendering can all cause incomplete captures.

Puppeteer's goto method accepts a waitUntil option:

await page.goto('https://example.com', {
  waitUntil: 'networkidle0' // wait until no network requests for 500ms
});

The available options are:

  • load — fires when the load event triggers (default)
  • domcontentloaded — fires when the HTML is parsed
  • networkidle0 — no more than 0 network connections for 500ms
  • networkidle2 — no more than 2 network connections for 500ms

For most screenshot use cases, networkidle0 gives the best results. But some pages with persistent connections (analytics, WebSockets) will never reach networkidle0. In those cases, use networkidle2 or add an explicit wait:

await page.goto('https://example.com', { waitUntil: 'domcontentloaded' });
await page.waitForSelector('.hero-image', { visible: true });
await page.screenshot({ path: 'screenshot.png' });

Output Formats

Puppeteer supports PNG, JPEG, and WebP:

// JPEG with quality setting (smaller file size)
await page.screenshot({
  path: 'screenshot.jpg',
  type: 'jpeg',
  quality: 80
});

// WebP for modern browsers
await page.screenshot({
  path: 'screenshot.webp',
  type: 'webp',
  quality: 85
});

JPEG and WebP produce significantly smaller files than PNG, which matters if you're generating images at scale.

Generating PDF Output

Puppeteer can also generate PDFs, which is useful for invoices, reports, or printable documents:

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

Common Production Challenges

Getting Puppeteer working locally is straightforward. Running it in production is where things get harder.

Memory and Resource Usage

Each headless Chrome instance uses 50-300 MB of RAM depending on the page. If you're handling multiple concurrent requests, memory adds up fast. You'll need to carefully manage browser instances — reusing them across requests while handling crashes and memory leaks.

// Reuse a single browser instance
let browser;

async function getScreenshot(url) {
  if (!browser) {
    browser = await puppeteer.launch({
      args: ['--no-sandbox', '--disable-setuid-sandbox']
    });
  }
  const page = await browser.newPage();
  try {
    await page.goto(url, { waitUntil: 'networkidle0', timeout: 30000 });
    return await page.screenshot({ type: 'png' });
  } finally {
    await page.close();
  }
}

Zombie Processes

Chrome processes can become orphaned if your application crashes or doesn't clean up properly. Over time, these zombie processes consume all available memory on the server. You'll want process monitoring and cleanup scripts to handle this.

Font Rendering

Headless Chrome on Linux renders fonts differently than on macOS or Windows. If your screenshots look off, you likely need to install font packages on the server:

apt-get install fonts-liberation fonts-noto-color-emoji fonts-noto-cjk

Even with the right fonts installed, rendering can differ between environments — a frustrating issue when screenshots look perfect locally but wrong in production.

Server Dependencies

Running Chrome on a server requires system-level dependencies (libx11, libxcomposite, libasound2, and others). Docker helps here, but adds another layer of infrastructure to manage. The official Puppeteer Docker images are a good starting point, but you'll still need to handle scaling, health checks, and resource limits.

Scaling

A single server can handle perhaps 5-10 concurrent screenshot requests before performance degrades. Beyond that, you need a queue system, load balancing, and multiple rendering nodes. This is where a simple "take a screenshot" requirement can turn into a distributed systems problem.

If you're evaluating alternatives, we've written a side-by-side comparison of Puppeteer and Playwright for screenshots, and a comparison of screenshot API services for when you want to skip browser management entirely.

When to Consider an API Instead

Puppeteer is a great tool if you need deep browser automation — filling out forms, clicking through flows, scraping content. For those use cases, there's no substitute for having full control.

But if your primary need is capturing web page screenshots — for social cards, link previews, documentation images, or visual testing — managing headless Chrome infrastructure is a significant ongoing cost. You're spending time on browser management instead of your actual product.

RenderScreenshot handles the browser infrastructure so you don't have to. A single API call replaces the Puppeteer boilerplate, process management, font configuration, and scaling concerns:

curl "https://api.renderscreenshot.com/v1/screenshot?url=https://example.com&width=1280&height=720" \
  -H "Authorization: Bearer rs_live_..."

No Chrome instances to manage, no zombie processes, no font issues across environments. Screenshots are captured on edge infrastructure and cached on a global CDN.

You can sign up for free and get 50 credits to try it out — enough to see if it fits your workflow before committing.


Have questions about migrating from Puppeteer? Check our documentation or reach out at [email protected].