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)
Memory per concurrent request
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 →