Chromium Screenshot in Node.js (2026)

Drive headless Chromium with Playwright or Puppeteer for local dev, CI pipelines, and serverless Lambda functions — plus a managed API option when you don't want the overhead.

Node.jsPlaywrightPuppeteer @sparticuz/chromiumLambdaApril 2026

Why Chromium for Screenshots?

Chromium is the rendering engine behind Chrome, Edge, and Brave. When you need pixel-accurate screenshots of modern websites — SPAs, CSS Grid layouts, WebGL, custom fonts — Chromium is the only reliable choice. Node.js gives you three main paths:

Each solves different deployment constraints. This guide covers all three, plus a managed API option for when you want zero infrastructure.

Option 1: Playwright (Recommended)

Playwright auto-downloads Chromium, Firefox, and WebKit. For screenshots you'll mainly use Chromium, but the API is consistent across all three browsers.

Install

npm install playwright
npx playwright install chromium   # ~150 MB Chromium binary

Basic screenshot

import { chromium } from 'playwright';

const browser = await chromium.launch({ headless: true });
const page = await browser.newPage();

await page.goto('https://example.com', { waitUntil: 'networkidle' });
await page.screenshot({ path: 'screenshot.png' });

await browser.close();

Full-page screenshot

await page.screenshot({
  path: 'full-page.png',
  fullPage: true          // captures the entire scrollable document
});

Viewport & device emulation

import { chromium, devices } from 'playwright';

const browser = await chromium.launch();
const context = await browser.newContext({
  ...devices['iPhone 15 Pro'],   // 393x852, 3x DPR
});
const page = await context.newPage();
await page.goto('https://example.com');
await page.screenshot({ path: 'mobile.png' });
await browser.close();

Screenshot a specific element

const el = page.locator('#hero-section');
await el.screenshot({ path: 'hero.png' });

Reuse browser context (high-throughput batch)

const browser = await chromium.launch();
const urls = ['https://a.com', 'https://b.com', 'https://c.com'];

// Reuse one browser, open parallel pages
await Promise.all(
  urls.map(async (url, i) => {
    const context = await browser.newContext();
    const page = await context.newPage();
    try {
      await page.goto(url, { waitUntil: 'networkidle', timeout: 15000 });
      await page.screenshot({ path: `out-${i}.png` });
    } finally {
      await context.close();
    }
  })
);
await browser.close();

Option 2: Puppeteer

Puppeteer is Google's official Chromium controller. It's slightly more verbose than Playwright but has excellent documentation and a massive ecosystem of plugins.

Install

npm install puppeteer   # includes Chromium ~150 MB
# or for serverless (no bundled Chromium):
npm install puppeteer-core

Basic screenshot

import puppeteer from 'puppeteer';

const browser = await puppeteer.launch({
  headless: 'new',        // use new headless mode (Chrome 112+)
  args: ['--no-sandbox', '--disable-setuid-sandbox']
});
const page = await browser.newPage();
await page.setViewport({ width: 1280, height: 800, deviceScaleFactor: 2 });

await page.goto('https://example.com', { waitUntil: 'networkidle2' });
const buffer = await page.screenshot({ encoding: 'binary', fullPage: true });
await browser.close();
// buffer is a Buffer — write to file or return from endpoint

Clip to region

await page.screenshot({
  clip: { x: 0, y: 0, width: 1200, height: 630 }  // OG image crop
});

Express screenshot endpoint with browser reuse

import express from 'express';
import puppeteer from 'puppeteer';

const app = express();
let browser;

async function getBrowser() {
  if (!browser || !browser.connected) {
    browser = await puppeteer.launch({ headless: 'new',
      args: ['--no-sandbox', '--disable-setuid-sandbox'] });
  }
  return browser;
}

app.get('/screenshot', async (req, res) => {
  const { url } = req.query;
  if (!url) return res.status(400).json({ error: 'url required' });

  const b = await getBrowser();
  const page = await b.newPage();
  try {
    await page.goto(url, { waitUntil: 'networkidle2', timeout: 20000 });
    const img = await page.screenshot({ fullPage: true });
    res.set('Content-Type', 'image/png');
    res.send(img);
  } finally {
    await page.close();
  }
});

app.listen(3000);

Option 3: @sparticuz/chromium for AWS Lambda

Lambda's read-only filesystem and 250 MB deployment limit make bundling a full Chromium binary tricky. @sparticuz/chromium solves this with a statically compiled, brotli-compressed binary that inflates to /tmp at cold-start.

Install

npm install @sparticuz/chromium puppeteer-core

Lambda handler

import chromium from '@sparticuz/chromium';
import puppeteer from 'puppeteer-core';

export const handler = async (event) => {
  const { url } = event;

  const browser = await puppeteer.launch({
    args: chromium.args,
    defaultViewport: chromium.defaultViewport,
    executablePath: await chromium.executablePath('/opt/nodejs/node_modules/@sparticuz/chromium/bin'),
    headless: chromium.headless,
  });

  const page = await browser.newPage();
  await page.goto(url, { waitUntil: 'networkidle2', timeout: 20000 });
  const screenshot = await page.screenshot({ encoding: 'base64', fullPage: true });
  await browser.close();

  return {
    statusCode: 200,
    headers: { 'Content-Type': 'image/png' },
    body: screenshot,
    isBase64Encoded: true,
  };
};
Lambda gotchas: Set memory to 1024 MB+ and timeout to 30+ seconds. Cold starts with Chromium inflate can take 3–5 seconds. Use Provisioned Concurrency if latency matters. Max deployment zip is 250 MB — use a Lambda Layer for the Chromium binary.

Option 4: SnapAPI — No Chromium Infrastructure

Self-hosting Chromium means managing browser crashes, memory leaks, zombie processes, and OS-level dependencies. SnapAPI handles all of that — you send a URL, get a screenshot back.

Quick start (3 lines)

const res = await fetch(
  `https://api.snapapi.pics/v1/screenshot?url=https://example.com&fullPage=true&format=png`,
  { headers: { 'X-Api-Key': 'YOUR_API_KEY' } }
);
const buffer = Buffer.from(await res.arrayBuffer());
// buffer is a PNG — write to file, upload to S3, return from endpoint

With the SDK

import { SnapAPI } from 'snapapi-js';

const snap = new SnapAPI({ apiKey: process.env.SNAPAPI_KEY });

const { data } = await snap.screenshot({
  url: 'https://example.com',
  fullPage: true,
  format: 'webp',
  width: 1440,
  height: 900,
  deviceScaleFactor: 2,
  blockAds: true,
  blockCookieBanners: true,
  delay: 1000,          // extra wait after load
  stealth: true,        // bypass bot detection
});

Device emulation via API

// 30+ preset devices — no Playwright devices import needed
const { data } = await snap.screenshot({
  url: 'https://example.com',
  device: 'iPhone 15 Pro',    // or 'Galaxy S23', 'iPad Air 5', etc.
  format: 'png',
});
SnapAPI free tier: 200 screenshots/month, no credit card. Stealth mode, device emulation, and ad blocking all included. Get your API key →

Which Approach Should You Use?

ScenarioBest ChoiceWhy
Local dev / testingPlaywrightBest DX, auto-installs Chromium, great debugger
CI pipeline (GitHub Actions)PlaywrightOfficial Docker images, parallel workers
Long-running Node serverPuppeteer (browser reuse)Browser pool pattern is well-documented
AWS Lambda / serverless@sparticuz/chromiumStatically compiled, fits Lambda constraints
Production API / SaaSSnapAPINo infra, autoscales, handles crashes for you
High volume (>10K/mo)SnapAPI Pro$79/mo for 50K captures vs EC2 + engineering time

Production Tips

Always close pages (not just the browser)

const page = await browser.newPage();
try {
  // ... your work
} finally {
  await page.close();  // releases memory even if an error occurred
}

Set explicit timeouts

// Playwright
await page.goto(url, { waitUntil: 'networkidle', timeout: 20000 });

// Puppeteer
await page.goto(url, { waitUntil: 'networkidle2', timeout: 20000 });

Block unnecessary resources to speed up captures

// Playwright — block images, fonts, media for faster text-only captures
await page.route('**/*', (route) => {
  const type = route.request().resourceType();
  if (['image', 'media', 'font'].includes(type)) return route.abort();
  return route.continue();
});
// NOTE: Don't block stylesheets — layout will break

Inject custom CSS before screenshotting

// Hide cookie banners, chat widgets, etc.
await page.addStyleTag({
  content: `
    #cookie-banner, .chat-widget, .intercom-launcher { display: none !important; }
    * { animation: none !important; transition: none !important; }
  `
});
await page.screenshot({ path: 'clean.png' });

Handle SPA loading properly

// waitUntil: 'networkidle' waits for no network activity for 500ms
// But some SPAs never go fully idle — use domcontentloaded + explicit wait
await page.goto(url, { waitUntil: 'domcontentloaded' });
await page.waitForSelector('#main-content', { timeout: 10000 });
await page.screenshot({ path: 'spa.png' });

Chromium Screenshots in GitHub Actions CI

# .github/workflows/screenshots.yml
name: Visual Screenshots
on: [push]

jobs:
  screenshot:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with: { node-version: '20' }
      - run: npm ci
      - run: npx playwright install chromium --with-deps
      - run: node scripts/take-screenshots.js
      - uses: actions/upload-artifact@v4
        with:
          name: screenshots
          path: screenshots/
CI tip: The mcr.microsoft.com/playwright:v1.50.0-noble Docker image pre-installs all browsers — use it to skip the playwright install step and speed up your pipeline.

Memory & Resource Management

Chromium processes are memory-hungry. Each browser context uses ~50–150 MB depending on the page. For high-throughput scenarios:

import { chromium } from 'playwright';
import genericPool from 'generic-pool';

const pool = genericPool.createPool({
  create: () => chromium.launch({ headless: true }),
  destroy: (browser) => browser.close(),
}, { min: 2, max: 5 });

async function screenshot(url) {
  const browser = await pool.acquire();
  const page = await browser.newPage();
  try {
    await page.goto(url, { waitUntil: 'networkidle', timeout: 15000 });
    return await page.screenshot({ fullPage: true });
  } finally {
    await page.close();
    pool.release(browser);
  }
}

// Graceful shutdown
process.on('SIGTERM', () => pool.drain().then(() => pool.clear()));

Summary

For most Node.js projects in 2026, Playwright is the go-to for local dev and CI — best API, best debugging, cross-browser. For AWS Lambda, use @sparticuz/chromium with puppeteer-core. For production APIs where you don't want to own Chromium infrastructure, SnapAPI takes 3 lines and handles crashes, scaling, and stealth for you.

The free tier (200/month) is enough to prototype. When you're ready to scale: grab a free API key and have screenshots running in minutes.