Chrome Node.js DevTools Protocol April 5, 2026

Headless Chrome API in Node.js: Complete Guide (2026)

Control Chrome programmatically for screenshots, scraping, PDF generation, and testing. Puppeteer vs Playwright vs raw CDP — when to use each.

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;
}
FeaturePuppeteerPlaywright
BrowsersChromium onlyChromium, Firefox, WebKit
Auto-waitingManualBuilt-in
Network interceptionBasicAdvanced (route + fulfill)
Browser contextsIncognito onlyFull isolation with storage
TracingBasicFull trace viewer
Mobile emulationYesYes + device descriptors
Maintained byGoogleMicrosoft

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-pool to 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-gpu flags. Mount /dev/shm as 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.