What Is Headless Chrome?
Headless Chrome is Chrome running without a visible window. It renders pages exactly like regular Chrome — full JavaScript execution, CSS rendering, Web APIs — but outputs to a buffer instead of a screen. Since Chrome 112 (2023), the new headless mode uses the same codebase as headed Chrome, eliminating the behavioral differences that plagued the old --headless flag.
Use cases: automated screenshots, web scraping, PDF generation, end-to-end testing, performance auditing, and pre-rendering SPAs for SEO.
Puppeteer: Google's Official Library
Puppeteer is Google's Node.js library for controlling Chrome. It downloads a compatible Chromium binary and provides a high-level API over the Chrome DevTools Protocol.
const puppeteer = require('puppeteer');
async function captureWithPuppeteer(url) {
const browser = await puppeteer.launch({
headless: 'new', // New headless mode (Chrome 112+)
args: ['--no-sandbox', '--disable-dev-shm-usage']
});
const page = await browser.newPage();
await page.setViewport({ width: 1280, height: 800 });
await page.goto(url, { waitUntil: 'networkidle0' });
// Screenshot
await page.screenshot({ path: 'capture.png', fullPage: true });
// PDF
await page.pdf({ path: 'capture.pdf', format: 'A4' });
// Extract text
const title = await page.$eval('h1', el => el.textContent);
await browser.close();
return title;
}
Playwright: Microsoft's Multi-Browser Tool
Playwright supports Chromium, Firefox, and WebKit from a single API. It has better auto-waiting, network interception, and context isolation than Puppeteer.
const { chromium } = require('playwright');
async function captureWithPlaywright(url) {
const browser = await chromium.launch();
const context = await browser.newContext({
viewport: { width: 1280, height: 800 },
userAgent: 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7)...',
locale: 'en-US',
timezoneId: 'America/New_York'
});
const page = await context.newPage();
// Intercept and block heavy resources
await page.route('**/*.{mp4,webm,ogg}', route => route.abort());
await page.goto(url, { waitUntil: 'networkidle' });
// Auto-waits for element before screenshotting
const hero = await page.locator('.hero-section');
await hero.screenshot({ path: 'hero.png' });
// Full page PDF
await page.pdf({ path: 'full.pdf', format: 'Letter' });
// Extract structured data
const links = await page.$$eval('a[href]', els =>
els.map(a => ({ text: a.textContent.trim(), href: a.href }))
);
await browser.close();
return links;
}
| Feature | Puppeteer | Playwright |
|---|---|---|
| Browsers | Chromium only | Chromium, Firefox, WebKit |
| Auto-waiting | Manual | Built-in |
| Network interception | Basic | Advanced (route + fulfill) |
| Browser contexts | Incognito only | Full isolation with storage |
| Tracing | Basic | Full trace viewer |
| Mobile emulation | Yes | Yes + device descriptors |
| Maintained by | Microsoft |
Raw Chrome DevTools Protocol
Both Puppeteer and Playwright are wrappers around CDP. When you need fine-grained control — custom performance metrics, heap snapshots, or protocol domains not exposed by the libraries — use CDP directly.
const CDP = require('chrome-remote-interface');
const { execSync } = require('child_process');
// Launch Chrome manually
execSync('google-chrome --headless --remote-debugging-port=9222 &');
async function rawCDP() {
const client = await CDP();
const { Page, Runtime, Network, DOM } = client;
await Network.enable();
await Page.enable();
// Intercept requests at protocol level
await Network.setRequestInterception({ patterns: [{ urlPattern: '*' }] });
Network.requestIntercepted(({ interceptionId, request }) => {
const blocked = /\.(woff2?|ttf|otf)$/i.test(request.url);
Network.continueInterceptedRequest({
interceptionId,
errorReason: blocked ? 'BlockedByClient' : undefined
});
});
await Page.navigate({ url: 'https://example.com' });
await Page.loadEventFired();
// Capture screenshot via protocol
const { data } = await Page.captureScreenshot({ format: 'png', fromSurface: true });
require('fs').writeFileSync('cdp-screenshot.png', Buffer.from(data, 'base64'));
// Get performance metrics
const { metrics } = await Runtime.evaluate({
expression: 'JSON.stringify(performance.getEntriesByType("navigation")[0])',
returnByValue: true
});
await client.close();
}
Raw CDP is powerful but verbose. Use it when libraries don't expose what you need — otherwise stick with Playwright or Puppeteer for maintainability.
Production Tips
- Browser pools: Use
generic-poolto maintain 2-6 browser instances. Creating a browser per request adds ~500ms latency and leaks memory. - Memory management: Close pages after use, restart browsers every ~100 operations. Chrome leaks ~2-5 MB per page even after closing.
- Timeouts: Set page-level timeouts (
page.setDefaultTimeout(30000)) and navigation timeouts. Pages that hang will consume your browser slot forever. - Resource blocking: Block fonts, videos, and large images you don't need. This cuts render time by 40-60%.
- Docker setup: Use
--no-sandbox --disable-dev-shm-usage --disable-gpuflags. Mount/dev/shmas tmpfs with at least 1GB. - Crash recovery: Listen for
browser.on('disconnected')and auto-restart. Chrome crashes under memory pressure — your app should survive it.
SnapAPI: Managed Headless Chrome API
Running headless Chrome in production means managing browser pools, memory leaks, crash recovery, proxy rotation, and queue systems. SnapAPI handles all of this — you get a REST API that does what headless Chrome does, without the infrastructure.
// Screenshot — no browser needed
const res = await fetch('https://api.snapapi.pics/v1/screenshot', {
method: 'POST',
headers: { 'Content-Type': 'application/json', 'X-Api-Key': 'sk_live_your_key' },
body: JSON.stringify({
url: 'https://example.com',
fullPage: true,
device: 'iPhone 15 Pro',
blockAds: true
})
});
const img = Buffer.from(await res.arrayBuffer());
// Scrape — rendered HTML from JS-heavy pages
const scrape = await fetch('https://api.snapapi.pics/v1/scrape', {
method: 'POST',
headers: { 'Content-Type': 'application/json', 'X-Api-Key': 'sk_live_your_key' },
body: JSON.stringify({ url: 'https://example.com', stealth: true })
});
const { html } = await scrape.json();
// Extract structured data — no parsing code needed
const extract = await fetch('https://api.snapapi.pics/v1/extract', {
method: 'POST',
headers: { 'Content-Type': 'application/json', 'X-Api-Key': 'sk_live_your_key' },
body: JSON.stringify({
url: 'https://example.com/products',
schema: {
products: [{ name: 'string', price: 'number', inStock: 'boolean' }]
}
})
});
const { data } = await extract.json();
SnapAPI covers screenshots, scraping, extraction, PDFs, video recording, and AI page analysis — everything you'd build with headless Chrome, accessible via REST. 200 free requests/month, 8 SDKs, and an MCP server for AI coding tools.