Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Use a real browser engine—Puppeteer or Playwright—to turn a DOM into an image in Node.js. A browser performs layout, loads fonts and images, executes JavaScript, and paints CSS. Then its screenshot API can save the whole page, the viewport, or one element as PNG, JPEG, or WebP. jsdom can build and modify a DOM, but it cannot render visual content by itself.

The reliable architecture

A DOM-to-image pipeline has two distinct jobs:

  1. Build state: your application or jsdom creates the HTML, data, and styles.
  2. Render and capture: Chromium, Firefox, or WebKit lays out that HTML and paints pixels through Puppeteer or Playwright.

The second step is essential. The jsdom documentation says that “jsdom does not have the capability to render visual content, and will act like a headless browser by default.” In practice, feeding HTML to jsdom and asking it for a PNG will not produce a faithful screenshot.

Capture a URL with Puppeteer

Install Puppeteer in a new Node.js project. The package downloads a compatible browser during installation.

npm init -y
npm install puppeteer

This complete script opens a page, waits for a useful readiness condition, and writes a full-page WebP image:

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.
const puppeteer = require('puppeteer');

(async () => {
  const browser = await puppeteer.launch({headless: true});
  try {
    const page = await browser.newPage();
    await page.setViewport({ width: 1440, height: 900, deviceScaleFactor: 1 });
    await page.goto('https://example.com', { waitUntil: 'networkidle2', timeout: 90000 });

    // Optional: wait for an application-specific signal.
    await page.waitForSelector('body');
    await page.screenshot({
      path: 'page.webp',
      fullPage: true,
      type: 'webp',
      quality: 88
    });
  } finally {
    await browser.close();
  }
})();

networkidle2 waits until network activity is low, but it is not a guarantee that your application is finished. Add a selector, a custom readiness flag, or an explicit font/image check when the page is data-driven.

Viewport versus full document

  • Omit fullPage (or set it to false) for exactly the visible viewport.
  • Use fullPage: true for the entire scrollable document. Very long pages can create large images and consume substantial memory.
  • Set deviceScaleFactor: 2 for a retina-style capture. Dimensions in the page remain CSS pixels, while the output contains more device pixels.

PNG, JPEG, and WebP

PNG is lossless and preserves text and transparency. JPEG is compact but has no alpha channel. WebP generally gives a smaller file; its quality value controls lossy compression. Choose the format your downstream storage or API accepts.

Capture one DOM element

Element screenshots avoid navigation bars, surrounding whitespace, and unrelated content. Puppeteer exposes ElementHandle.screenshot():

const puppeteer = require('puppeteer');

(async () => {
  const browser = await puppeteer.launch({ headless: true });
  try {
    const page = await browser.newPage();
    await page.setViewport({ width: 1200, height: 800, deviceScaleFactor: 1 });
    await page.goto('https://example.com', { waitUntil: 'networkidle2' });
    const card = await page.waitForSelector('.product-card', { visible: true });
    if (!card) throw new Error('The .product-card element was not found');
    await card.screenshot({ path: 'product-card.png', type: 'png' });
  } finally {
    await browser.close();
  }
})();

Use a stable class or data attribute rather than a generated CSS class. If the selector matches several nodes, choose one explicitly with page.$$('.product-card') and capture the desired handle.

What’s actually slowing this PC down?

Pick the symptom - the matching free tool is one click away.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Do the same with Playwright

Playwright provides page-level and locator-level screenshots and can drive Chromium, Firefox, or WebKit.

npm init -y
npm install playwright
npx playwright install chromium
const { chromium } = require('playwright');

(async () => {
  const browser = await chromium.launch({ headless: true });
  try {
    const context = await browser.newContext({
      viewport: { width: 1440, height: 900 },
      deviceScaleFactor: 1
    });
    const page = await context.newPage();
    await page.goto('https://example.com', { waitUntil: 'networkidle', timeout: 90000 });
    await page.screenshot({ path: 'screenshot.png', fullPage: true });

    const locator = page.locator('main');
    await locator.screenshot({ path: 'main.webp', type: 'webp', quality: 90 });
  } finally {
    await browser.close();
  }
})();

Playwright’s locator screenshot automatically targets the element represented by the locator. Its screenshot options also support full-page capture, PNG/JPEG/WebP output, and CSS-pixel or device-pixel scaling. Use the browser engine that matches your production environment when rendering differences matter.

Rendering HTML that exists only in memory

For a string of HTML, create a page and use page.setContent(). Include a complete document so relative styles and fonts behave predictably.

const { chromium } = require('playwright');

const html = `<!doctype html>
<html>
<head>
  <meta charset="utf-8">
  <style>
    html, body { margin: 0; }
    .badge { width: 640px; padding: 32px; font: 700 32px system-ui; background: #101827; color: white; }
  </style>
</head>
<body><div class="badge">Generated in Node.js</div></body>
</html>`;

(async () => {
  const browser = await chromium.launch();
  try {
    const page = await browser.newPage({ viewport: { width: 704, height: 200 } });
    await page.setContent(html, { waitUntil: 'load' });
    await page.locator('.badge').screenshot({ path: 'badge.png' });
  } finally {
    await browser.close();
  }
})();

If the markup refers to local files, serve it from a local HTTP server or use absolute URLs. Browser security rules, relative paths, and font loading are more predictable over HTTP than from an arbitrary file: URL.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Using jsdom as a preparation step

When an application already uses jsdom to construct or transform HTML, serialize the resulting document and pass it to a real browser. A documented jsdom-screenshot pattern reads document.documentElement.outerHTML, serves that markup through a local web server, launches Puppeteer, waits for resources, and captures the result. It exposes viewport, target-selector, screenshot, and interception options.

const { JSDOM } = require('jsdom');
const { chromium } = require('playwright');

(async () => {
  const dom = new JSDOM('<!doctype html><body><div id="report"></div></body>');
  const report = dom.window.document.querySelector('#report');
  report.innerHTML = '<h1>Invoice</h1><p>Ready to render</p>';
  const markup = dom.serialize();

  const browser = await chromium.launch();
  try {
    const page = await browser.newPage({ viewport: { width: 900, height: 600 } });
    await page.setContent(markup, { waitUntil: 'load' });
    await page.locator('#report').screenshot({ path: 'report.png' });
  } finally {
    await browser.close();
  }
})();

This bridge renders the serialized HTML, but it does not magically reproduce browser APIs that your original application expects. If scripts depend on network requests, canvas, layout measurements, or fonts, run those scripts in the real page and wait for their completion.

Make captures deterministic

Wait for fonts and images

await page.evaluate(async () => {
  await document.fonts.ready;
  const images = Array.from(document.images);
  await Promise.all(images.map(img => img.complete
    ? Promise.resolve()
    : new Promise(resolve => {
        img.addEventListener('load', resolve, { once: true });
        img.addEventListener('error', resolve, { once: true });
      })));
});

Wait for application state, not an arbitrary sleep

A fixed delay can be too short on a busy runner and unnecessarily slow on a fast one. Prefer a selector such as [data-rendered="true"], a response wait, or an application flag:

await page.waitForSelector('[data-rendered="true"]', { timeout: 30000 });

Freeze motion and choose a stable environment

Animations can capture different frames on each run. Inject CSS before the screenshot:

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.
await page.addStyleTag({ content: `*, *::before, *::after {
  animation: none !important;
  transition: none !important;
  caret-color: transparent !important;
}` });

Visual differences can still come from operating systems, font rendering, animations, and GPU behavior. For pixel comparisons, pin the browser version, install the same fonts, use the same viewport and scale, and run comparisons in a consistent CI image. The jsdom-screenshot project describes its approach as experimental and specifically warns about these differences.

Options that affect the image

  • Scope: page or one element/locator; full document or viewport.
  • Geometry: viewport width and height, element clipping, and device scale factor.
  • Output: PNG, JPEG, or WebP; JPEG/WebP quality; output path or returned bytes.
  • Browser state: cookies, local storage, authentication headers, user agent, timezone, and geolocation.
  • Content control: hide selectors, click before capture, inject CSS or JavaScript, and block selected requests.

Keep these settings in source control alongside the test or rendering job. A changed viewport or font package is a visual change, not merely an infrastructure detail.

Troubleshooting

“The image is blank”

Check that navigation succeeded, the selector exists, and the page is not waiting on a failed API call. Capture a diagnostic screenshot after navigation and log the final URL and console errors. Wait for the actual content selector instead of only a timeout.

“Fonts or images are missing”

Wait for document.fonts.ready and image completion. Confirm that the browser process can reach the asset host and that relative URLs resolve from the page’s origin. Self-host fonts in CI when possible.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

“ElementHandle is null”

The selector did not match before the timeout. Verify the selector in browser devtools, wait for the component’s mount signal, and avoid selectors tied to generated class names.

“Navigation timed out”

Raise the timeout only after finding the slow dependency. Use a targeted readiness condition and, where appropriate, an explicit waitUntil mode. A page that never finishes analytics requests may not become idle; waiting for a known content marker is safer.

“Screenshots differ between machines”

Use the same browser engine and version, viewport, device scale, fonts, OS image, and animation policy. Differences from GPU and font rasterization can remain even when the DOM is identical.

“The process runs out of memory”

Reuse one browser for a batch, close pages and contexts promptly, avoid enormous full-page captures, and process URLs in bounded batches. Prefer element screenshots when a full document is unnecessary.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.
Independent reader supportYour contribution helps us test, update, and keep practical guides available for everyone.Support on Ko-Fi

Or skip the browser setup

ScreenshotNeo is a website screenshot API and MCP server. One request returns a PNG, JPEG, WebP, or PDF, with browser setup handled for you:

curl -G "https://api.screenshotneo.com/v1/shot" -d access_key=YOUR_API_KEY --data-urlencode url=https://stripe.com -o shot.webp

See the ScreenshotNeo documentation for all options. It accepts cookie and consent banners as a visitor and removes more than 60 known consent platforms, newsletter popups, and chat widgets before capture; each step can be turned off. Bot checks or CAPTCHAs, blank pages, timeouts, failed loads, and cache hits are not billed, and response headers identify the page verdict and billing status. Its MCP server provides take_screenshot, get_page_info, and capture_pdf tools for Claude, Cursor, and other MCP clients. The Free plan includes 1,000 screenshots per month with no card; paid plans start at $5 for 3,000 shots. Create a free ScreenshotNeo account.

Performance, reliability, and cost decisions

Local Puppeteer or Playwright gives maximum control and no per-capture service charge, but your team owns browser downloads, sandbox configuration, fonts, concurrency, retries, and network access. A hosted API trades that setup for request latency and usage pricing. Cache stable captures, reuse browser processes, and avoid waiting for every network request when a deterministic application signal is available.

For CI, record the browser version, viewport, scale, URL, readiness condition, and output format with each artifact. For production, set navigation and overall job timeouts, retry transient network failures, and treat authentication data as secrets. Never place access keys, cookies, or Authorization headers in client-side code or public logs.

Free tools Windows power users keep installed

One-click scans. No signup required.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Which approach should you choose?

Need Best fit Reason
Full control in a Node.js test or worker Puppeteer Direct page and element screenshot APIs with Chromium automation.
Multiple browser engines or locator-oriented tests Playwright Page and locator screenshots plus Chromium, Firefox, and WebKit support.
DOM construction without visual layout jsdom Useful for state preparation, not for painting pixels.
Many URLs without maintaining browsers ScreenshotNeo Hosted capture, clean shots, billing verdict headers, and an MCP server.

Frequently Asked Questions

Can I screenshot a DOM node without loading the whole page?

A real browser still needs to load the document and styles, but Puppeteer’s ElementHandle.screenshot() or Playwright’s locator.screenshot() can save only the selected node.

Does a screenshot contain the live DOM?

No. PNG, JPEG, and WebP are raster images. Preserve the HTML separately if you need an editable or inspectable representation.

Is a fixed timeout ever sufficient?

It can be a fallback, but a selector, readiness flag, or explicit font/image check is more reliable across machines and network conditions.

Product prices and availability are accurate as of the date/time indicated and are subject to change. Any price and availability information displayed on Amazon at the time of purchase will apply.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.