SnapDOMGitHub8K
Release · 3.0.0

SnapDOM v3 is out

Automatic capture reuse, incremental recapture, fewer setup options and a live comparison you can run on your own machine.

SnapDOM 3.0.0 is out, along with v3 of the official plugins. I put a lot of work into this release, especially into making repeated captures cheaper without returning stale content. I'm happy to finally ship it.

If you're new to SnapDOM, it captures a DOM element with its styles and assets, then lets you export SVG, PNG, JPG, WebP, canvas or blobs. It runs in the browser. The familiar snapdom(element, options) entry point stays the same in v3; much of the work happened underneath it.

The second capture matters

A share card may be exported several times without changing. A dashboard may update one value between captures. Rebuilding the whole capture every time repeats style reads, cloning and asset work that could still be valid.

In v3, the first successful capture of an eligible static element creates a memo. A later capture can reuse it if the relevant state is unchanged. For supported local mutations, the engine rebuilds the affected subtrees and splices them into the previous capture. This happens automatically.

Working out when that shortcut is safe took a lot of care. A local DOM edit can affect layout elsewhere. A scroll or focus change can alter the image without changing the text. Canvas, video and iframe contents can change without a useful DOM mutation at all.

The incremental path is deliberately conservative: it runs only when the engine can preserve the full capture's output. Other changes fall back to a full capture, and frame-driven content captures fresh. Direct stylesheet edits such as sheet.insertRule() still need invalidate: true, because they do not produce the DOM mutations used for style invalidation. The cache guide explains those boundaries.

Image exports from an existing result still use its captured state. If the page changes, call snapdom() again to read that change. PNG encoding and rasterization also still cost time; reusing capture work does not remove those steps.

Concurrent captures now keep their working state in separate sessions. Capturing two different elements at once no longer relies on one shared mutable session inside the engine.

Fonts by default, and a lot of small rendering fixes

Web fonts now embed automatically when the captured content uses them. The default is embedFonts: 'auto'; a capture using only system fonts skips that work. You can still pass false to leave text fonts out or true to force discovery.

A large part of the release also went into rendering details. Fractional widths could introduce an extra line wrap. Pseudo-element boxes, scrolled containers and images could end up misaligned. Transformed and offscreen elements needed fixes, too. These are easy defects to notice in an export: a label wraps, an icon moves, or a shadow ends up in the wrong place.

V3 fixes those cases, preserves inherited fill colors in SVG <use> icons, and corrects how slotted Shadow DOM content is cloned. Font handling also preserves icon styles and keeps unrelated font families when an icon font shares their stylesheet. The changelog links to the individual fixes and reports.

Two APIs for the work around a capture

fromString() handles the temporary mount when your input is HTML rather than an element already on the page. It places the markup offscreen in the live document, captures it and removes the mount. Page CSS and fonts apply.

import { snapdom } from '@zumer/snapdom'

const capture = await snapdom.fromString(
  '<article class="share-card"><h2>SnapDOM v3</h2></article>'
)

const image = await capture.toPng({ width: 1200, dpr: 1 })
document.body.append(image)

That string goes into the real document through innerHTML. Use trusted markup or sanitize it first. This API still needs a browser DOM.

preCapture() prepares eligible captures on user intent. Enable it once, then keep the capture call in your control's handler. For a button and card already mounted in your app:

const button = document.querySelector('#export-button')
const card = document.querySelector('#card')

snapdom.preCapture()

button.addEventListener('click', () =>
  snapdom.download(card, { format: 'png', filename: 'card.png' })
)

The first capture started during the control's press or click event teaches SnapDOM which element and options belong to that control. On later hover or focus, it can prepare the same capture before the next click. It has to learn that association first, and a capture deferred to an unrelated later timer will not teach it. There are no arguments to configure and no background polling.

Core and official plugins share the v3 release line

The official @zumer/snapdom-plugins package is on v3 as well. It covers HTML exports, text and JSON context, annotated element maps, image-based PDFs, image effects, and GIF or video recording. Match its major version to the core when you upgrade. Recording exports take new frames from the live element when recording starts.

Plugins also get capture geometry, the exact export options and canvas cropping support. Those details matter when an export has to line up with the source interface or when a plugin needs to use the same crop as a core exporter. The plugin gallery has examples, and the plugin reference documents the hooks and custom exports.

The default renderer is still SVG. The experimental native html-in-canvas engine requires a compatible browser and a custom build; it is omitted from the standard bundle.

Run the comparison on your own machine

I also worked on the site alongside this release. The reference, capture recipes and framework guides cover different parts of an integration. The capture boundaries article explains where SnapDOM runs when Node, browser automation or cross-origin frames are involved.

The part I'd particularly like people to try is the live comparison. Pick a scene and the libraries you want to compare, then run it in your own browser, on your own hardware. It includes html2canvas, html-to-image, modern-screenshot and other DOM capture libraries.

Every library is timed through to a PNG data URL. First and repeat captures have separate columns, and the page shows the rendered output alongside the timings. The regular scenes disable SnapDOM's capture memoization; choose the Polling scene to exercise automatic reuse. Compare the images as well as the timings: a fast result with missing content is still a broken export. Pixel differences are measured against SnapDOM's output, which is a reference image rather than an independent correctness verdict.

There is also a capability matrix that captures small fixtures and checks whether their marker colors survive. It tests features such as Shadow DOM, pseudo-elements and painted canvas content. The results come from running the captures in your browser, so the scene, browser and hardware all matter.

Before upgrading from v2

This is a major release. Read the migration guide before changing the version in an existing app. A few changes deserve a close look:

  • width and height take precedence over scale. To request a width of 400, use width: 400 instead of width: 200, scale: 2. Raster dpr still applies; use dpr: 1 when you need that exact pixel width.
  • Remove preCache and its subpath import. Normal capture reuse is automatic, and preCapture() learns capture intent. It is not a direct rename.
  • Remove the old fast, burst, compress, resolvePicturePlaceholders and pictureResolver options. Those public switches are no longer supported. Handle custom image loading and timeouts in your app before capture.
  • Core redaction masks password fields only. Use the redactInputs() plugin if your application needs other visible fields masked.
  • afterExport hooks observe the export result; their return values no longer become the next hook's payload. Use defineExports for custom output.

filter and exclude remain supported together, each with its own hide/remove mode. Function-valued filter, exclude, excludeStyleProps and fallbackURL make each new capture run fresh so callbacks can read current application state. The v2 documentation and v2 branch remain available for projects staying on that release.

npm install @zumer/snapdom@3

# If your app uses the official plugins:
npm install @zumer/snapdom-plugins@3

Thanks to everyone who sent bug reports and small reproductions. They helped track down several of the rendering fixes in this release. Try v3 on your own UI, and if a capture looks wrong, open an issue with the browser, the source element and the exported result.