Front-end Page Screenshot: dom-to-image and html2canvas – Usage and Implementation Principles
This article compares the front‑end screenshot libraries dom‑to‑image and html2canvas, explains their APIs and underlying SVG‑or‑canvas rendering pipelines, walks through typical usage code, highlights common pitfalls such as cross‑origin images and scrolling, and offers practical configuration and debugging tips for developers.
This article introduces two popular front‑end page screenshot solutions— dom‑to‑image and html2canvas —including their usage APIs, core implementation principles, and practical tips for developers.
Background
Page screenshot is a common requirement in front‑end development, especially for marketing scenarios where a visual snapshot provides richer information than a simple link.
Related Technologies
The two most widely used npm libraries are:
dom‑to‑image: https://github.com/tsayen/dom-to-image
html2canvas: https://github.com/niklasvh/html2canvas
Both libraries convert a DOM node into an image, but they use different rendering pipelines: dom‑to‑image relies on SVG, while html2canvas renders onto a Canvas.
1. dom‑to‑image
Core API list:
toSvg (DOM → SVG)
toPng (DOM → PNG)
toJpeg (DOM → JPEG)
toBlob (DOM → binary Blob)
toPixelData (DOM → raw pixel array)
Simple usage to generate a PNG image:
import domtoimage from "domtoimage"
const node = document.getElementById('node');
domtoimage.toPng(node, options).then((dataUrl) => {
const img = new Image();
img.src = dataUrl;
document.body.appendChild(img);
});Key options include filter , backgroundColor , width , height , style , quality , imagePlaceholder , and cacheBust .
Implementation Analysis
The conversion flow is roughly:
toPng → calls draw → obtains a Canvas and calls canvas.toDataURL() .
draw → calls toSvg to get an SVG data URL, creates an Image from it, draws the Image onto a new Canvas.
toSvg → clones the DOM ( cloneNode ), inlines fonts and images, embeds the cloned markup into a <foreignObject> inside an SVG, and returns a data URL.
cloneNode recursively copies the DOM tree, handling <canvas> elements, computed styles, pseudo‑elements, and form inputs.
makeSvgDataUri serialises the cloned node with XMLSerializer , wraps it in a <foreignObject> , and builds the final SVG data URL.
function toPng(node, options) {
return draw(node, options || {})
.then((canvas) => canvas.toDataURL());
}
function draw(domNode, options) {
return toSvg(domNode, options)
.then(util.makeImage)
.then(util.delay(100))
.then((image) => {
const canvas = newCanvas(domNode);
canvas.getContext("2d").drawImage(image, 0, 0);
return canvas;
});
function newCanvas(domNode) {
const canvas = document.createElement("canvas");
canvas.width = options.width || util.width(domNode);
canvas.height = options.height || util.height(domNode);
if (options.bgcolor) {
const ctx = canvas.getContext("2d");
ctx.fillStyle = options.bgcolor;
ctx.fillRect(0, 0, canvas.width, canvas.height);
}
return canvas;
}
}
function toSvg(node, options) {
options = options || {};
copyOptions(options);
return Promise.resolve(node)
.then((node) => cloneNode(node, options.filter, true))
.then(embedFonts)
.then(inlineImages)
.then(applyOptions)
.then((clone) => makeSvgDataUri(clone, options.width || util.width(node), options.height || util.height(node)));
}
function cloneNode(node, filter, root) {
if (!root && filter && !filter(node)) return Promise.resolve();
return Promise.resolve(node)
.then(makeNodeCopy)
.then((clone) => cloneChildren(node, clone, filter))
.then((clone) => processClone(node, clone));
function makeNodeCopy(node) {
if (node instanceof HTMLCanvasElement) return util.makeImage(node.toDataURL());
return node.cloneNode(false);
}
// ... (recursive cloning logic omitted for brevity)
}
function makeSvgDataUri(node, width, height) {
return Promise.resolve(node)
.then((node) => {
node.setAttribute("xmlns", "http://www.w3.org/1999/xhtml");
return new XMLSerializer().serializeToString(node);
})
.then(util.escapeXhtml)
.then((xhtml) => `<foreignObject x="0" y="0" width="100%" height="100%">${xhtml}</foreignObject>`)
.then((foreignObject) => `<svg xmlns="http://www.w3.org/2000/svg" width="${width}" height="${height}">${foreignObject}</svg>`)
.then((svg) => `data:image/svg+xml;charset=utf-8,${svg}`);2. html2canvas
html2canvas uses a Canvas‑based rendering pipeline. It manually paints the DOM onto a Canvas, which means some CSS properties may not be fully supported.
Typical usage:
import html2canvas from 'html2canvas';
html2canvas(dom, option).then((canvas) => {
const dataUrl = canvas.toDataURL();
// use dataUrl …
});Common options (full list at http://html2canvas.hertzen.com/configuration ) include CORS handling, background color, scaling, logging, etc.
Compatibility: Firefox 3.5+, Chrome, Opera 12+, IE9+, Edge, Safari 6+.
Implementation Overview
The library follows these high‑level steps:
Build a configuration object from user options and defaults (resourceOptions, contextOptions, windowOptions, cloneOptions, renderOptions).
Clone the target node and its children, inlining images and fonts.
Render the cloned DOM into an off‑screen iframe to obtain computed styles.
Parse the DOM tree into a set of internal data structures ( ElementContainer , ElementPaint , StackingContext ) that capture bounds, styles, and stacking order.
Traverse the stacking contexts and draw each element onto the Canvas according to CSS stacking rules (negative z‑index, positioned elements, floats, etc.).
Export the final Canvas as a data URL or Blob.
async renderStackContent(stack) {
await this.renderNodeBackgroundAndBorders(stack.element);
for (const child of stack.negativeZIndex) await this.renderStack(child);
await this.renderNodeContent(stack.element);
for (const child of stack.nonInlineLevel) await this.renderNode(child);
// … (other stacking‑order loops omitted for brevity)
}
async renderNode(paint) {
if (paint.container.styles.isVisible()) {
await this.renderNodeBackgroundAndBorders(paint);
await this.renderNodeContent(paint);
}
}Common Issues & Solutions
Incomplete screenshot : Scroll the page to the top before capturing. document.documentElement.scrollTop = 0; document.body.scrollTop = 0;
Cross‑origin images : Add crossorigin="anonymous" to <img> tags or convert the image to a Base64 data URL before drawing. function getUrlBase64_pro(len, url) { const canvas = document.createElement("canvas"); const ctx = canvas.getContext("2d"); return new Promise((resolve, reject) => { const img = new Image(); img.crossOrigin = "Anonymous"; img.onload = function () { canvas.width = canvas.height = len; ctx.drawImage(img, 0, 0, len, len); resolve(canvas.toDataURL("image/")); }; img.onerror = reject; img.src = url; }); }
Elements that should be excluded : Use the ignoreElements callback or add data-html2canvas-ignore to the element. html2canvas(ele, { useCORS: true, ignoreElements: (element) => element.tagName.toLowerCase() === 'iframe' });
Conclusion
The article provides a comparative overview of two front‑end screenshot libraries, detailed usage examples, and a deep dive into their internal rendering mechanisms, helping developers choose and troubleshoot the appropriate solution for their projects.
Tencent Cloud Developer
Official Tencent Cloud community account that brings together developers, shares practical tech insights, and fosters an influential tech exchange community.
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.