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.
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
scalecontrols output resolution. Usingwindow.devicePixelRatioproduces a sharper image but increases memory use.backgroundColorsets the canvas background; usenullwhen transparency is required.useCORSasks the browser to use CORS-enabled images. The image server must send suitable CORS headers.allowTaintcan permit tainted canvases, but a tainted canvas cannot be exported safely withtoDataURL()ortoBlob(); do not treat it as a fix for cross-origin export.ignoreElementscan exclude an element, for example a live chat launcher or a button.windowWidth,windowHeight,x,y,widthandheightlet you define the virtual capture area.onclonelets 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
- Launch a known browser version and create a fresh context.
- Set viewport, device scale factor, locale, timezone, color scheme and permissions explicitly.
- Navigate with a timeout and a deliberate wait condition.
networkidleis useful for mostly static pages, but an application with polling may never become idle. - Wait for a meaningful selector such as
[data-ready="true"], or wait for a bounded delay after the page signals readiness. - Disable animations and hide transient UI before capture.
- Capture the viewport, locator or full page, then close the context and browser in a
finallyblock.
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
scaleincrease 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.
