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

To generate an image from a DOM in Node.js, render the DOM in a real browser engine, then call its screenshot API. Puppeteer and Playwright both support page screenshots and element screenshots. jsdom can build or modify the DOM, but it cannot lay out or paint visual content by itself; send its serialized HTML to a browser for the actual image.

The reliable architecture

A screenshot has three distinct stages:

  1. Build state: load the application or create markup, then wait for data, fonts, images, and other resources.
  2. Render: let Chromium, Firefox, or WebKit perform CSS layout, painting, and compositing.
  3. Encode: save the rendered pixels as PNG, JPEG, or WebP (or produce a PDF when required).

A DOM implementation alone is not a renderer. The jsdom documentation states that “jsdom does not have the capability to render visual content, and will act like a headless browser by default.” Use jsdom for server-side DOM manipulation, not as the final screenshot engine.

Capture a page with Puppeteer

Install and run a minimal script

Install Puppeteer in a Node.js project. Its installation downloads a compatible browser unless you deliberately configure an existing executable.

npm install puppeteer

Create capture.js:

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' });
    await page.screenshot({ path: 'page.png', fullPage: true });
  } finally {
    await browser.close();
  }
})();

Run it with node capture.js. networkidle2 waits until network activity is low, but it is not a guarantee that application data or web fonts are ready. Add an application-specific readiness check whenever possible.

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

Capture one DOM element

Use an element screenshot when the output should contain a card, chart, invoice, or other component rather than the entire document.

const card = await page.$('[data-testid="receipt"]');
if (!card) throw new Error('Receipt element was not found');
await card.screenshot({ path: 'receipt.png' });

The selector must identify a stable element. A generated class name or an element that appears only after a race-prone animation can make captures intermittent.

Wait for application state, fonts, and images

Prefer a deterministic signal emitted by your application:

await page.goto('http://localhost:3000/report', { waitUntil: 'domcontentloaded' });
await page.waitForSelector('[data-rendered="true"]');
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 });
      })));
});
await page.screenshot({ path: 'report.webp', type: 'webp', fullPage: true });

For pages without a readiness marker, combine a selector check with a short, purposeful delay. A fixed long timeout is slower and still fails when a backend response takes longer than expected.

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

Capture with Playwright

Page and full-document screenshots

Playwright offers the same basic model and can drive Chromium, Firefox, or WebKit.

npm install playwright
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' });
    await page.screenshot({ path: 'screenshot.png', fullPage: true });
  } finally {
    await browser.close();
  }
})();

The fullPage option expands the capture to the document’s scrollable height. Without it, the image is the current viewport.

Locator screenshots and output formats

const chart = page.locator('#sales-chart');
await chart.waitFor({ state: 'visible' });
await chart.screenshot({ path: 'chart.jpg', type: 'jpeg', quality: 90 });

Playwright supports page and locator screenshots, PNG, JPEG, and WebP output (subject to the browser and API version), full-page capture, and CSS-pixel or device-pixel scaling. Use PNG for lossless text and diagrams; JPEG for photographic content; WebP when a smaller modern-image payload is acceptable.

Generate an image from HTML created by jsdom

When your source is a jsdom document, serialize it and serve it through a local HTTP server. Then let Puppeteer render that URL. This preserves the separation between DOM construction and visual rendering.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.
const http = require('node:http');
const { JSDOM } = require('jsdom');
const puppeteer = require('puppeteer');

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

  const server = http.createServer((req, res) => {
    res.writeHead(200, { 'content-type': 'text/html; charset=utf-8' });
    res.end(html);
  });
  await new Promise(resolve => server.listen(0, '127.0.0.1', resolve));
  const { port } = server.address();

  const browser = await puppeteer.launch({ headless: true });
  try {
    const page = await browser.newPage();
    await page.goto(`http://127.0.0.1:${port}/`, { waitUntil: 'networkidle2' });
    await page.screenshot({ path: 'jsdom-rendered.png', fullPage: true });
  } finally {
    await browser.close();
    server.close();
  }
})();

This pattern also lets you add a stylesheet, external fonts, images, a target selector, request interception, or a controlled viewport before capture. If the markup references relative assets, serve those assets from the same test server or rewrite the URLs to reachable locations.

Choose the capture scope and rendering options

Need Use Important setting
Visible browser area Page screenshot Set viewport dimensions first
Entire scrollable document Page screenshot fullPage: true
One component Element or locator screenshot Wait for a stable selector
High-density output Page or element screenshot Set deviceScaleFactor (Puppeteer) or context scale
Small photographic file JPEG or WebP Set format and quality deliberately
Pixel-perfect text and diagrams PNG Keep fonts and environment consistent

Hide unrelated content with CSS or capture a specific element. Disable carousels, blinking cursors, and transitions before taking visual-regression images. A simple injected rule is:

await page.addStyleTag({ content: `
  *, *::before, *::after {
    animation: none !important;
    transition: none !important;
    caret-color: transparent !important;
  }
` });

Reliability, performance, and reproducibility

  • Reuse a browser process: launch once and create a fresh page or context per job; browser startup is usually more expensive than navigation.
  • Bound every wait: set navigation and application-level timeouts, then close pages in finally blocks so failed jobs do not leak processes.
  • Control inputs: fix viewport, device scale, timezone, locale, color scheme, and user-agent when those values affect layout.
  • Make assets deterministic: pin web-font versions, wait for document.fonts.ready, and avoid time-dependent content.
  • Limit full-page size: very tall pages create large bitmaps and consume memory. Capture a component or split long documents when possible.
  • Use consistent CI images: visual output can vary with operating system, font rendering, animations, and GPU behavior. The jsdom-screenshot project describes its browser-rendering approach as experimental and warns about these differences.

Common failures and fixes

“The image is blank”

The page may still be loading, the selector may be hidden, or a bot check may have replaced the content. Wait for a meaningful selector, inspect the page HTML and console logs, and verify that the URL is reachable from the machine running the browser.

Fonts or icons are missing

Capture occurs before web fonts finish loading, or the font host rejects the browser request. Await document.fonts.ready, allow the font origin in your network policy, and use the same font files in CI and local runs.

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.

Images are not present in a full-page shot

Lazy-loaded images may require scrolling or an application signal before they load. Scroll through the document, wait for image completion, or use the application’s “content ready” event before calling the screenshot method.

Element lookup fails

The selector may be evaluated before client-side rendering completes, or it may match an unstable class. Wait for the element and add a durable data-testid or ID intended for automation.

Navigation times out

Do not automatically increase the timeout indefinitely. Check DNS, TLS, authentication, redirects, blocked third-party requests, and pages that keep long-polling connections open. Use domcontentloaded plus an explicit readiness condition when network idle never occurs.

Local HTML cannot load its assets

A file:// page often has the wrong base URL and restrictive cross-origin behavior. Serve the serialized DOM over localhost, as in the jsdom example, and provide routes for stylesheets, scripts, fonts, and images.

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.
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 provides a hosted screenshot API and MCP server when you do not want to maintain browser binaries and capture code. It accepts a URL and returns PNG, JPEG, WebP, or PDF. Before capture it accepts the cookie or consent banner like a visitor and removes more than 60 known consent platforms, newsletter popups, and chat widgets. Bot checks or CAPTCHAs, blank pages, timeouts, failed loads, and cache hits are not billed; response headers report the page verdict and whether the request was billed.

One call from Node.js:

const q = new URLSearchParams({ access_key: 'YOUR_API_KEY', url: 'https://stripe.com' });
const res = await fetch(`https://api.screenshotneo.com/v1/shot?${q}`);
if (!res.ok) throw new Error(`ScreenshotNeo returned ${res.status}`);
const image = Buffer.from(await res.arrayBuffer());
require('node:fs').writeFileSync('shot.webp', image);

Equivalent cURL and Python calls are useful in scripts and CI:

curl -G "https://api.screenshotneo.com/v1/shot" -d access_key=YOUR_API_KEY --data-urlencode url=https://stripe.com -o shot.webp
import requests
r = requests.get("https://api.screenshotneo.com/v1/shot", params={"access_key": "YOUR_API_KEY", "url": "https://stripe.com"}, timeout=90)
r.raise_for_status()
open("shot.webp", "wb").write(r.content)

See the ScreenshotNeo documentation for the 63 capture options, including full-page and CSS-selector captures, device presets and custom viewports, retina scale, dark mode, custom CSS and JavaScript, clicks, waits, request blocking, headers, cookies, user agents, timezone and geolocation, transparent backgrounds, resizing, TTL caching, signed image links, asynchronous webhooks, bulk capture, usage data, and an OpenAPI specification. Its MCP server exposes take_screenshot, get_page_info, and capture_pdf to 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 screenshots; every feature is included on every plan. Create a free ScreenshotNeo account to try it.

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

Puppeteer or Playwright?

Consideration Puppeteer Playwright
Basic page capture page.screenshot() page.screenshot()
Component capture ElementHandle.screenshot() locator.screenshot()
Full-page capture Supported Supported
Browser engines Chromium-focused workflow Chromium, Firefox, and WebKit projects
Best fit Existing Chromium/Puppeteer automation Cross-browser coverage and locator-oriented tests

The documented screenshot controls overlap substantially. Choose the library already used by your test or automation stack, then verify behavior in the exact package and browser versions used in production.

Frequently asked implementation questions

Can jsdom take a screenshot without Chromium?

No. It can construct and serialize the DOM, but a visual browser must perform layout and painting.

Should I use a page or element screenshot?

Use a page screenshot for a viewport or whole document; use an element or locator screenshot for a self-contained component.

Why do identical screenshots differ in CI?

Fonts, operating-system rasterization, animations, GPU behavior, timing, and device scale can all change pixels. Standardize those inputs and wait for stable application state.

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

Is a timeout a readiness condition?

A timeout only delays the capture. A selector, data attribute, network condition, or explicit resource check expresses what “ready” means and is more dependable.

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.