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:
- Playwright — Microsoft-maintained, cross-browser, excellent API, best for 2026
- Puppeteer — Google-maintained, Chrome-specific, slightly more battle-tested for PDF
- @sparticuz/chromium — Statically compiled Chromium for AWS Lambda / serverless
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?
| Scenario | Best Choice | Why |
| Local dev / testing | Playwright | Best DX, auto-installs Chromium, great debugger |
| CI pipeline (GitHub Actions) | Playwright | Official Docker images, parallel workers |
| Long-running Node server | Puppeteer (browser reuse) | Browser pool pattern is well-documented |
| AWS Lambda / serverless | @sparticuz/chromium | Statically compiled, fits Lambda constraints |
| Production API / SaaS | SnapAPI | No 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.