ScreenshotNeo

BlogComparisons

Client-Side vs. Server-Side Web Page Screenshots

Learn when to use html2canvas in the browser or Playwright/Puppeteer on a server, with runnable code, caveats, troubleshooting, and a hosted option.

By the ScreenshotNeo team1 October 20269 min read

Client-side and server-side screenshots produce images in fundamentally different ways. A client-side library such as html2canvas runs inside the page, traverses its DOM and reconstructs an image from the properties it understands. It does not read the browser’s final pixels. A server-side tool such as Playwright or Puppeteer drives a browser and captures the rendered page.

Use client-side capture when an in-page, approximate image is acceptable and you cannot run a separate browser. Use server-side automation when the image must match rendered output, when navigation and browser settings are part of the job, or when capture runs on a server. The right choice depends on fidelity, security boundaries, control and operations; the official documentation does not establish a universal speed or accuracy winner.

What “client-side” and “server-side” mean

Concern Client-side DOM reconstruction Server-side browser capture
Where code runs In the user’s browser page; it needs window, document and computed styles. In a server process that launches or connects to a browser.
Image source A rebuilt representation of DOM and supported CSS. The browser’s rendered page pixels.
Typical tools html2canvas. Playwright or Puppeteer.
Navigation control Limited to the page already loaded by the user. Navigate, set viewport, wait for conditions, inject scripts and capture.
Main risks Unsupported CSS, cross-origin resources and canvas size limits. Browser and host differences, resource usage and browser lifecycle failures.

Client-side screenshots with html2canvas

html2canvas’s documentation describes a script that creates a “screenshot” in the user’s browser, then explains that it traverses the DOM and builds a representation from available page information. Only CSS properties implemented by the library are rendered. The result can therefore differ from what a user sees in the browser.

Minimal browser example

Include the library, select an element and export the generated canvas. This complete page captures the element with ID invoice and downloads a PNG.

<!doctype html>
<html>
<body>
  <section id="invoice">
    <h1>Invoice 1042</h1>
    <p>Captured in the browser.</p>
  </section>
  <button id="save">Save screenshot</button>

  <script src="https://cdn.jsdelivr.net/npm/html2canvas@1.4.1/dist/html2canvas.min.js"></script>
  <script>
    document.querySelector('#save').addEventListener('click', async () => {
      const element = document.querySelector('#invoice');
      const canvas = await html2canvas(element, {
        backgroundColor: '#ffffff',
        scale: window.devicePixelRatio,
        useCORS: true,
        logging: false
      });
      const link = document.createElement('a');
      link.download = 'invoice.png';
      link.href = canvas.toDataURL('image/png');
      link.click();
    });
  </script>
</body>
</html>

Options that matter

  • scale controls output resolution. Using window.devicePixelRatio produces a sharper image but increases memory use.
  • backgroundColor sets the canvas background; use null when transparency is required.
  • useCORS asks the browser to use CORS-enabled images. The image server must send suitable CORS headers.
  • allowTaint can permit tainted canvases, but a tainted canvas cannot be exported safely with toDataURL() or toBlob(); do not treat it as a fix for cross-origin export.
  • ignoreElements can exclude an element, for example a live chat launcher or a button.
  • windowWidth, windowHeight, x, y, width and height let you define the virtual capture area.
  • onclone lets you modify the cloned document before rendering, useful for hiding controls or adding a print-only style.

Client-side limits and security boundaries

Images generally need to be same-origin or served with CORS headers. html2canvas can recurse into same-origin iframes, but cross-origin iframes cannot be read because of browser security restrictions. Sandboxed frames without allow-same-origin have the same limitation. The library also cannot reproduce CSS it does not implement, so filters, complex blending, some generated content, form controls and browser-native UI may differ.

Canvas dimensions are limited by the browser and platform. The html2canvas FAQ warns that exceeding a limit can produce blank or partially rendered output; limits vary, so test the largest page and scale you plan to support. A tall, high-DPI full-page canvas is especially likely to hit memory or dimension limits.

Server-side screenshots with Playwright

Playwright launches a real browser, navigates to a URL and calls the Page screenshot API. It can capture the viewport, a selected element or the full scrollable page, and can write PNG, JPEG or WebP. The browser renders CSS, fonts, images and JavaScript before capture.

Install and run

npm init -y
npm install playwright
npx playwright install chromium
// screenshot.mjs
import { chromium } from 'playwright';

const browser = await chromium.launch({ headless: true });
const context = await browser.newContext({
  viewport: { width: 1440, height: 900 },
  deviceScaleFactor: 1,
  colorScheme: 'light'
});
const page = await context.newPage();
await page.goto('https://example.com', { waitUntil: 'networkidle' });
await page.screenshot({ path: 'page.webp', fullPage: true, type: 'webp', quality: 85 });
await browser.close();

For a selected element, replace the final call with await page.locator('.hero').screenshot({ path: 'hero.png' });. For a viewport-only image, omit fullPage. Playwright also supports animations: 'disabled' and transparent backgrounds where the page and output format allow them.

Reliable capture sequence

  1. Launch a known browser version and create a fresh context.
  2. Set viewport, device scale factor, locale, timezone, color scheme and permissions explicitly.
  3. Navigate with a timeout and a deliberate wait condition. networkidle is useful for mostly static pages, but an application with polling may never become idle.
  4. Wait for a meaningful selector such as [data-ready="true"], or wait for a bounded delay after the page signals readiness.
  5. Disable animations and hide transient UI before capture.
  6. Capture the viewport, locator or full page, then close the context and browser in a finally block.
import { chromium } from 'playwright';

const browser = await chromium.launch();
try {
  const page = await browser.newPage({ viewport: { width: 1280, height: 800 } });
  await page.goto('https://example.com/dashboard', { waitUntil: 'domcontentloaded', timeout: 30000 });
  await page.addStyleTag({ content: '* { animation: none !important; transition: none !important; }' });
  await page.locator('[data-dashboard-ready="true"]').waitFor({ state: 'visible', timeout: 15000 });
  await page.screenshot({ path: 'dashboard.png', fullPage: true });
} finally {
  await browser.close();
}

Puppeteer alternative

Puppeteer exposes the same broad model through Page.screenshot(). The API returns screenshot data when no path is supplied and coordinates with other operations in a BrowserContext.

import puppeteer from 'puppeteer';

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

Choosing the method

Requirement Best starting point Reason
A user clicks “export” on the current page html2canvas No server browser or upload is required.
Pixel output should match a controlled browser Playwright or Puppeteer The browser performs the rendering and capture.
Capture a URL from a backend job Playwright or Puppeteer The worker can navigate independently of a user’s session.
Cross-origin images and iframes are essential Server browser, subject to access controls Client-side origin rules can prevent DOM access.
Very large full-page output Server browser with tested dimensions Client canvas limits vary and can yield blank output.
Repeatable visual regression tests Playwright Use a pinned browser and host environment, then compare screenshots.

Cross-environment fidelity

Server capture is closer to rendered pixels, but it is not automatically identical everywhere. Playwright’s visual comparison guidance lists host operating system, browser version, settings, hardware, power source and headless mode as variables that can change rendering. Pin the browser and fonts, keep the capture image stable, and record viewport, scale, locale, timezone and color scheme with each baseline.

Client capture varies with the user’s browser, zoom, device pixel ratio, loaded fonts and page state. If the output is an audit artifact or a regression baseline, collect it in a controlled environment instead of relying on arbitrary user devices.

Troubleshooting

Symptom Likely cause Fix
html2canvas throws a security error or the export is blank A cross-origin image or iframe tainted the canvas. Serve assets from the same origin, enable CORS on the asset server, or capture with a server browser.
A CSS effect is missing The property is unsupported or only partially implemented. Check html2canvas’s supported CSS behavior, add a capture-only fallback style, or use Playwright/Puppeteer.
Only part of a tall page appears Canvas dimension or memory limits. Reduce scale, capture sections, or use a server browser and test its maximum dimensions.
html2canvas does not work in Node.js It depends on browser globals and computed styles. Run it in the page, or use Playwright/Puppeteer for Node.js.
Playwright times out at networkidle Analytics, sockets or polling keep requests active. Use domcontentloaded, then wait for a specific ready selector with a bounded timeout.
Fonts or images are missing in a server shot Capture began before resources loaded, or the worker cannot reach them. Wait for a readiness signal, verify network access and preload critical fonts.
Screenshots differ between CI and a laptop Different browser, OS, fonts, scale or headless settings. Pin the environment and compare only artifacts produced by that environment.
Full-page capture repeats or clips sticky content The page changes while Playwright scrolls or uses fixed-position elements. Freeze animations and data updates, hide sticky elements for the capture, and test the page’s full-scroll behavior.

Performance, reliability and cost

Client-side

  • Work happens on the user’s CPU and memory. Larger DOM trees and higher scale increase blocking time and canvas memory.
  • There is no browser startup cost, but the result depends on the current page state and network-loaded assets.
  • Use a lower scale for previews, capture only the needed element, and move long captures behind a user action.

Server-side

  • Launching a browser is expensive compared with a normal HTTP request. Reuse a browser process when safe, create isolated contexts per job, and cap concurrency to protect memory.
  • Set navigation and selector timeouts, cancel stuck jobs, close pages in finally, and retry only failures that are safe to repeat.
  • Cache immutable pages or final image bytes when the URL and capture settings are unchanged. Keep browser and font versions pinned for reproducibility.
  • No controlled head-to-head benchmark was identified in the official sources, so choose from requirements rather than an assumed speed ranking.

Or skip the browser setup

ScreenshotNeo is a hosted screenshot API and MCP server. It accepts one GET request and returns PNG, JPEG, WebP or PDF. Before capture it accepts consent banners and removes more than 60 known consent platforms, newsletter popups and chat widgets; each step can be turned off. Bot checks, CAPTCHAs, blank pages, timeouts, failed loads and cache hits are not billed, and the response identifies the result with X-Page-Verdict and X-Billed headers. Its MCP server provides take_screenshot, get_page_info and capture_pdf for Claude, Cursor and other MCP clients.

See the ScreenshotNeo API documentation for all options. The same request works from cURL, Python and Node.js:

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)
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(`Screenshot failed: ${res.status}`);
const fs = await import('node:fs/promises');
await fs.writeFile('shot.webp', Buffer.from(await res.arrayBuffer()));

ScreenshotNeo also supports full-page capture with lazy images loaded, CSS element capture, dark mode, 12 device presets or custom viewports, retina scale, PDF paper and page options, custom CSS and JavaScript, clicks, selector waits, delays, network-idle waits, request blocking, headers, cookies, user agents, authorization, timezone, geolocation, transparent backgrounds, resizing, configurable caching, signed image links, asynchronous jobs with signed webhooks, bulk capture of up to 100 URLs per call, a usage API and an OpenAPI spec. Parameter names used by other screenshot APIs work too, which can simplify migration.

Plans include 1,000 screenshots per month free with no card; paid plans start at $5 for 3,000 screenshots. Create a free ScreenshotNeo account.

FAQ

Can html2canvas capture what a user sees exactly?

No. It reconstructs an image from DOM and supported style information, so unsupported CSS, browser-native UI and cross-origin resources can differ.

Can I run html2canvas in a Node.js backend?

No. Its FAQ says it relies on browser APIs such as window, document and computed styles. Use a browser automation library for Node.js.

Is a server-side screenshot always more accurate?

It captures rendered browser output, but the output still depends on browser, OS, fonts, hardware and settings. Control those variables before judging fidelity.

Should I wait for network idle?

Only when the page can become idle. Applications with polling or sockets should use a readiness selector or application signal with a bounded timeout.

How do I make visual tests reproducible?

Pin browser and fonts, fix viewport and device scale, set locale, timezone and color scheme, disable animations, and run comparisons in the same environment.