Tutorial

Take a Screenshot in Node.js in 2026: 4 Methods With Code

There are several ways to take a screenshot of a webpage in Node.js. This guide covers them all with working code, and explains clearly which to use for your situation.

The quick answer: Playwright for scripts and E2E tests, a screenshot API for production web apps. Read on for the reasoning and code.

Method 1: Puppeteer

npm install puppeteer
import puppeteer from 'puppeteer';

async function screenshot(url, outputPath) {
  const browser = await puppeteer.launch({
    headless: 'new',
    args: ['--no-sandbox', '--disable-setuid-sandbox']  // required in Docker/CI
  });

  const page = await browser.newPage();

  // Set viewport before navigation
  await page.setViewport({ width: 1440, height: 900, deviceScaleFactor: 2 }); // 2x for retina

  await page.goto(url, {
    waitUntil: 'networkidle2',  // wait until fewer than 2 network connections for 500ms
    timeout: 30000
  });

  // Optional: wait for a specific element to be visible
  // await page.waitForSelector('.main-content', { visible: true });

  const screenshot = await page.screenshot({
    path: outputPath,      // omit to return Buffer
    type: 'png',           // 'jpeg' or 'webp' also supported
    fullPage: true,        // capture entire scrollable page
    // clip: { x: 0, y: 0, width: 800, height: 600 }  // crop to region
  });

  await browser.close();
  return screenshot; // Buffer if no path provided
}

// Usage
await screenshot('https://example.com', './screenshot.png');

Puppeteer in a loop (batch screenshots)

import puppeteer from 'puppeteer';

async function batchScreenshots(urls) {
  // Reuse browser across requests — critical for performance
  const browser = await puppeteer.launch({ headless: 'new' });

  const results = [];
  for (const url of urls) {
    const page = await browser.newPage();
    await page.setViewport({ width: 1440, height: 900 });
    await page.goto(url, { waitUntil: 'networkidle2' });
    const buf = await page.screenshot({ fullPage: true });
    await page.close(); // close page, not browser
    results.push({ url, buffer: buf });
  }

  await browser.close(); // close once when done
  return results;
}

Key pattern: reuse the browser, close pages. Launching a new browser per request is the #1 performance mistake with Puppeteer — it takes 1-3 seconds per launch and uses ~300MB of memory each time.

Method 2: Playwright

npm install playwright
npx playwright install chromium  # downloads Chromium
import { chromium } from 'playwright';

async function screenshot(url) {
  const browser = await chromium.launch();
  const context = await browser.newContext({
    viewport: { width: 1440, height: 900 },
    deviceScaleFactor: 2,
  });
  const page = await context.newPage();

  await page.goto(url, { waitUntil: 'networkidle' });

  // Wait for fonts to load (prevents FOUT in screenshots)
  await page.evaluate(() => document.fonts.ready);

  const buffer = await page.screenshot({
    type: 'png',
    fullPage: true,
  });

  await browser.close();
  return buffer;
}

Playwright vs Puppeteer for screenshots

The APIs are nearly identical. Playwright has two advantages for screenshots:

  • Browser contexts — isolate cookies between screenshots without launching a new browser
  • document.fonts.ready — more reliably waits for font rendering
  • Auto-waiting — Playwright waits for elements to be stable before screenshotting

For most use cases, the difference is marginal. If you're already using Puppeteer, don't switch just for screenshots.

Method 3: Screenshot API (Best for Production)

Instead of running Chromium in your Node.js process, you make an HTTP call to an API that handles the browser. No binary, no memory, no crashes.

npm install snapapi-js  # optional — or use native fetch
// With native fetch (Node.js 18+)
async function screenshot(url) {
  const res = await fetch('https://api.snapapi.pics/v1/screenshot', {
    method: 'POST',
    headers: {
      'X-Api-Key': process.env.SNAPAPI_KEY,
      'Content-Type': 'application/json',
    },
    body: JSON.stringify({
      url,
      format: 'png',
      full_page: true,
      viewport_width: 1440,
      viewport_height: 900,
    }),
  });

  if (!res.ok) throw new Error(`Screenshot failed: ${res.status}`);

  const { url: screenshotUrl } = await res.json();
  return screenshotUrl; // CDN URL valid for 24h
}

// Usage
const url = await screenshot('https://example.com');
console.log(url); // https://cdn.snapapi.pics/screenshots/abc123.png

Download as Buffer (if you need the bytes)

async function screenshotAsBuffer(url) {
  // Step 1: generate screenshot
  const apiRes = await fetch('https://api.snapapi.pics/v1/screenshot', {
    method: 'POST',
    headers: { 'X-Api-Key': process.env.SNAPAPI_KEY, 'Content-Type': 'application/json' },
    body: JSON.stringify({ url, format: 'png', full_page: true }),
  });
  const { url: screenshotUrl } = await apiRes.json();

  // Step 2: download bytes
  const imgRes = await fetch(screenshotUrl);
  const buffer = Buffer.from(await imgRes.arrayBuffer());
  return buffer;
}

Advanced options

const res = await fetch('https://api.snapapi.pics/v1/screenshot', {
  method: 'POST',
  headers: { 'X-Api-Key': process.env.SNAPAPI_KEY, 'Content-Type': 'application/json' },
  body: JSON.stringify({
    url: 'https://example.com',
    format: 'jpeg',
    jpeg_quality: 90,
    full_page: false,
    viewport_width: 375,
    viewport_height: 812,
    device: 'iPhone 14',      // 30+ device presets
    block_ads: true,
    block_cookie_banners: true,
    wait_for: '.main-chart',  // wait for CSS selector
    inject_css: 'body { font-size: 16px !important; }',
    delay: 2000,              // extra wait in ms for animations
    dark_mode: true,
  })
});

Express.js Integration

import express from 'express';

const app = express();
app.use(express.json());

// Screenshot endpoint
app.post('/screenshot', async (req, res) => {
  const { url, format = 'png', full_page = true } = req.body;

  if (!url) return res.status(400).json({ error: 'url required' });

  try {
    const apiRes = await fetch('https://api.snapapi.pics/v1/screenshot', {
      method: 'POST',
      headers: { 'X-Api-Key': process.env.SNAPAPI_KEY, 'Content-Type': 'application/json' },
      body: JSON.stringify({ url, format, full_page }),
    });

    if (!apiRes.ok) {
      const err = await apiRes.json();
      return res.status(502).json({ error: 'Screenshot failed', detail: err });
    }

    const data = await apiRes.json();
    res.json({ screenshotUrl: data.url });

  } catch (err) {
    res.status(500).json({ error: err.message });
  }
});

app.listen(3000);

Next.js API Route

// app/api/screenshot/route.js (Next.js App Router)
export async function POST(request) {
  const { url } = await request.json();

  if (!url) {
    return Response.json({ error: 'url required' }, { status: 400 });
  }

  const res = await fetch('https://api.snapapi.pics/v1/screenshot', {
    method: 'POST',
    headers: {
      'X-Api-Key': process.env.SNAPAPI_KEY,
      'Content-Type': 'application/json',
    },
    body: JSON.stringify({ url, format: 'png', full_page: true }),
  });

  if (!res.ok) {
    return Response.json({ error: 'Screenshot failed' }, { status: 502 });
  }

  const data = await res.json();
  return Response.json({ url: data.url });
}
Note: Puppeteer and Playwright don't work in Vercel Edge Runtime or Cloudflare Workers — Chromium can't run there. An HTTP-based screenshot API is the only option for edge deployments.

Performance Comparison

First screenshot latency (cold)

Puppeteer (cold browser)
3,200ms
Playwright (cold browser)
2,900ms
SnapAPI (warm pool)
680ms

Memory per concurrent request

Puppeteer (per instance)
~180MB
Playwright (per instance)
~160MB
SnapAPI (HTTP call)
<1MB

With browser reuse, Puppeteer and Playwright latency drops to ~800ms for subsequent screenshots (browser already running). The API stays at ~400-700ms consistently because it uses pre-warmed browser instances from a pool.

Comparison Table

Method Serverless Memory Cold start Full JS Cost
Puppeteer No 180MB+ 3-5s Free + compute
Playwright No 160MB+ 2-4s Free + compute
SnapAPI Yes <1MB none Free tier → $19/mo

Which to Use

CLI script, cron job, or E2E test? Puppeteer or Playwright. You control the environment, memory doesn't matter, and you don't need to pay for an API.

Web app API endpoint? Screenshot API. Your users want fast responses, not 3-second browser boot times. HTTP calls are stateless, don't leak memory, and work from any deployment environment.

Serverless (Lambda, Vercel, Cloudflare Workers)? Screenshot API only. Chromium is too large for most serverless environments and doesn't work in edge runtimes at all.

High concurrency (>10 simultaneous screenshots)? Screenshot API. You'd need to build a browser pool, queue, and memory manager to safely run Puppeteer at that scale — which is essentially re-building what the API provides.

Screenshot any page from Node.js with one fetch call

200 free screenshots/month, no credit card. Works in Express, Next.js, Lambda, and Cloudflare Workers.

Get your free API key →