DEV Community

Dennis
Dennis

Posted on

Taking Screenshots in Node.js: Puppeteer, Playwright, and API Methods

You can take a screenshot of any URL in Node.js using Puppeteer, Playwright, or a screenshot API. Puppeteer and Playwright launch a headless browser locally, giving you full control but requiring Chrome/Chromium as a dependency. Screenshot APIs handle the browser remotely and return an image over HTTP, cutting setup to a single npm install.

This guide covers all three methods with working code, then compares them so you can pick the right tool for your project.

Method 1: Puppeteer

Puppeteer is Google's official Node.js library for controlling Chrome via the DevTools Protocol. It ships its own Chromium binary (~300 MB download), so you get a consistent browser without managing system dependencies manually.

Install

npm install puppeteer
Enter fullscreen mode Exit fullscreen mode

This downloads Chromium automatically. If you already have Chrome installed and want to skip the download:

npm install puppeteer-core
Enter fullscreen mode Exit fullscreen mode

With puppeteer-core, you need to point executablePath to your Chrome binary.

Basic Screenshot

const puppeteer = require('puppeteer');

async function takeScreenshot(url, outputPath) {
  const browser = await puppeteer.launch();
  const page = await browser.newPage();
  await page.goto(url, { waitUntil: 'networkidle2' });
  await page.screenshot({ path: outputPath });
  await browser.close();
}

takeScreenshot('https://example.com', 'screenshot.png');
Enter fullscreen mode Exit fullscreen mode

networkidle2 waits until there are no more than 2 network connections for 500ms. That handles most pages, but SPAs with long-polling connections sometimes need a different strategy.

Full-Page Capture

By default, Puppeteer captures only the viewport. To grab the entire scrollable page:

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

One thing to watch: pages with infinite scroll or lazy-loaded content below the fold won't fully render. The screenshot captures what the DOM contains at that moment, not what a user would see after scrolling.

Custom Viewport and Mobile Emulation

const browser = await puppeteer.launch();
const page = await browser.newPage();

// Desktop: 1920x1080
await page.setViewport({ width: 1920, height: 1080 });
await page.goto('https://example.com', { waitUntil: 'networkidle2' });
await page.screenshot({ path: 'desktop.png' });

// Mobile: iPhone 14 dimensions with device scale factor
await page.setViewport({
  width: 390,
  height: 844,
  deviceScaleFactor: 3,
  isMobile: true,
  hasTouch: true
});
await page.goto('https://example.com', { waitUntil: 'networkidle2' });
await page.screenshot({ path: 'mobile.png' });

await browser.close();
Enter fullscreen mode Exit fullscreen mode

Puppeteer also ships built-in device descriptors:

const iPhone = puppeteer.KnownDevices['iPhone 14'];
await page.emulate(iPhone);
Enter fullscreen mode Exit fullscreen mode

Waiting Strategies

Choosing the right wait condition is the single biggest source of flaky screenshots. Here are your options:

Strategy What It Does Best For
load Waits for the load event Simple static pages
domcontentloaded DOM parsed, subresources still loading Fast captures where you don't need images
networkidle0 No network connections for 500ms Pages that finish loading cleanly
networkidle2 2 or fewer connections for 500ms Pages with analytics, tracking pixels

For SPAs that render client-side, you often need to wait for a specific element:

await page.goto('https://spa-app.com');
await page.waitForSelector('#main-content', { timeout: 10000 });
await page.screenshot({ path: 'spa.png' });
Enter fullscreen mode Exit fullscreen mode

Or wait for a fixed delay as a last resort:

await page.goto('https://heavy-animations.com');
await new Promise(resolve => setTimeout(resolve, 3000));
await page.screenshot({ path: 'animated.png' });
Enter fullscreen mode Exit fullscreen mode

PDF Output

Puppeteer can generate PDFs, but only in headless mode:

const browser = await puppeteer.launch();
const page = await browser.newPage();
await page.goto('https://example.com', { waitUntil: 'networkidle2' });

await page.pdf({
  path: 'page.pdf',
  format: 'A4',
  printBackground: true,
  margin: { top: '20mm', bottom: '20mm', left: '15mm', right: '15mm' }
});

await browser.close();
Enter fullscreen mode Exit fullscreen mode

printBackground is important. Without it, you lose background colors and images.

Handling Common Issues

Cookie consent banners block the content you want to capture. You can dismiss them:

await page.goto(url, { waitUntil: 'networkidle2' });
try {
  await page.click('[class*="cookie"] button', { timeout: 3000 });
  await new Promise(resolve => setTimeout(resolve, 500));
} catch {
  // No cookie banner found, continue
}
await page.screenshot({ path: 'clean.png' });
Enter fullscreen mode Exit fullscreen mode

Hiding elements (ads, popups, sticky headers):

await page.evaluate(() => {
  document.querySelectorAll('[class*="popup"], [class*="modal"]')
    .forEach(el => el.style.display = 'none');
});
Enter fullscreen mode Exit fullscreen mode

This works but it's brittle. Selectors change, new popups appear, and you end up maintaining a growing list of CSS selectors.

Method 2: Playwright

Playwright is Microsoft's browser automation library. The API is similar to Puppeteer, but it supports Chromium, Firefox, and WebKit out of the box.

Install

npm install playwright
npx playwright install
Enter fullscreen mode Exit fullscreen mode

The second command downloads all three browser binaries. To install only Chromium:

npx playwright install chromium
Enter fullscreen mode Exit fullscreen mode

Basic Screenshot

const { chromium } = require('playwright');

async function takeScreenshot(url, outputPath) {
  const browser = await chromium.launch();
  const page = await browser.newPage();
  await page.goto(url, { waitUntil: 'networkidle' });
  await page.screenshot({ path: outputPath });
  await browser.close();
}

takeScreenshot('https://example.com', 'screenshot.png');
Enter fullscreen mode Exit fullscreen mode

The API is almost identical to Puppeteer. The main differences:

  • waitUntil: 'networkidle' instead of networkidle0/networkidle2
  • Auto-waiting on selectors (no need for explicit waitForSelector before clicking)
  • page.locator() for more resilient element targeting

Full-Page and Viewport

const { chromium } = require('playwright');

async function captureFullPage(url) {
  const browser = await chromium.launch();
  const context = await browser.newContext({
    viewport: { width: 1280, height: 720 }
  });
  const page = await context.newPage();
  await page.goto(url, { waitUntil: 'networkidle' });

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

  await browser.close();
}
Enter fullscreen mode Exit fullscreen mode

Cross-Browser Screenshots

This is where Playwright shines. You can capture the same URL in three engines:

const { chromium, firefox, webkit } = require('playwright');

async function crossBrowserScreenshots(url) {
  for (const browserType of [chromium, firefox, webkit]) {
    const browser = await browserType.launch();
    const page = await browser.newPage();
    await page.goto(url, { waitUntil: 'networkidle' });
    await page.screenshot({
      path: `screenshot-${browserType.name()}.png`
    });
    await browser.close();
  }
}

crossBrowserScreenshots('https://example.com');
Enter fullscreen mode Exit fullscreen mode

If you're doing visual regression testing across browsers, Playwright is the obvious choice.

Device Emulation

const { chromium, devices } = require('playwright');

const iPhone = devices['iPhone 14'];

async function mobileScreenshot(url) {
  const browser = await chromium.launch();
  const context = await browser.newContext({ ...iPhone });
  const page = await context.newPage();
  await page.goto(url, { waitUntil: 'networkidle' });
  await page.screenshot({ path: 'mobile.png' });
  await browser.close();
}
Enter fullscreen mode Exit fullscreen mode

Playwright's device list is extensive and regularly updated.

Method 3: Screenshot APIs

Both Puppeteer and Playwright require you to run a headless browser. That means managing Chromium binaries, handling memory usage, dealing with zombie processes, and tuning timeouts for every edge case.

Screenshot APIs handle all of that remotely. You send an HTTP request with the URL and options, and you get an image back.

Using SnapRender's Node.js SDK

npm install snaprender
Enter fullscreen mode Exit fullscreen mode
const { SnapRender } = require('snaprender');
const fs = require('fs');

const client = new SnapRender({ apiKey: 'sk_live_...' });
const buffer = await client.screenshot({
  url: 'https://example.com',
  format: 'png',
  width: 1280,
  height: 720,
  full_page: true
});

fs.writeFileSync('screenshot.png', buffer);
Enter fullscreen mode Exit fullscreen mode

Features like ad blocking, cookie banner removal, dark mode, and device emulation are just parameters. No CSS selector hunting, no page.evaluate() hacks.

Using Raw fetch

If you don't want an SDK dependency, the API is a single GET request:

const url = new URL('https://app.snap-render.com/v1/screenshot');
url.searchParams.set('url', 'https://example.com');
url.searchParams.set('format', 'png');
url.searchParams.set('width', '1280');
url.searchParams.set('full_page', 'true');

const response = await fetch(url, {
  headers: { 'X-API-Key': 'sk_live_...' }
});

const buffer = Buffer.from(await response.arrayBuffer());
fs.writeFileSync('screenshot.png', buffer);
Enter fullscreen mode Exit fullscreen mode

No browser binary. No process management. The heavy lifting happens on the API's servers.

Comparison

Factor Puppeteer Playwright Screenshot API
Install size ~300 MB (includes Chromium) ~400 MB (three browsers) ~50 KB (SDK only)
Setup time 5-10 min 5-10 min 2 min
Lines of code for basic screenshot 8-10 8-10 3-5
Full-page capture Built-in Built-in Built-in
Mobile emulation Built-in Built-in Built-in
Ad/cookie banner removal Manual CSS selectors Manual CSS selectors Single parameter
Dark mode forcing Inject CSS / prefers-color-scheme Inject CSS / prefers-color-scheme Single parameter
Browser engines Chromium only* Chromium, Firefox, WebKit Managed remotely
Concurrency Limited by system RAM Limited by system RAM Handled by API
Server dependency Runs locally Runs locally Requires network
Cost Free (compute costs) Free (compute costs) Free tier + paid plans
Best for Browser automation beyond screenshots Cross-browser testing Production screenshot pipelines

*Puppeteer experimentally supports Firefox, but it's not production-ready.

Memory and Performance Considerations

Each Puppeteer/Playwright browser instance uses 100-300 MB of RAM. If you're capturing screenshots concurrently, that adds up fast. On a typical 2 GB server, you can run maybe 4-6 browser instances before things start swapping.

Common production issues with self-hosted headless browsers:

  • Zombie processes: If your script crashes without calling browser.close(), Chrome processes pile up. You need process monitoring.
  • Memory leaks: Long-running browser instances leak memory over time. Most production setups restart the browser every N screenshots.
  • Font rendering: Different OS environments render fonts differently. Your local screenshots won't match your CI screenshots.
  • GPU/sandbox issues: Docker containers need --no-sandbox and --disable-gpu flags, which have security implications.

Screenshot APIs eliminate these operational concerns. You're trading control for reliability.

Which Method Should You Use?

Use Puppeteer if you need browser automation beyond screenshots. Form filling, navigation testing, scraping, and E2E testing all benefit from Puppeteer's full browser control. If screenshots are just one small part of a bigger automation workflow, Puppeteer makes sense.

Use Playwright if you need cross-browser testing. Capturing the same page in Chromium, Firefox, and WebKit with a single API is Playwright's strongest selling point. It also has better auto-waiting behavior and a more robust selector engine than Puppeteer.

Use a screenshot API if screenshots are the product feature, not a side effect of testing. Building an OG image service, a link preview system, a visual monitoring tool, or any feature where the output is the screenshot itself. The setup is minimal, scaling is someone else's problem, and edge cases like cookie banners and ad blocking are already solved.

For most production use cases where you need to take a screenshot of a URL in Node.js, the code-to-value ratio of an API is hard to beat. But if you're already running Puppeteer or Playwright for testing, adding screenshot capture to that existing setup costs you nothing.

Top comments (0)