A full-page DOM capture can succeed as SVG and still fail at the next step: turning that SVG into one enormous bitmap.
That is the problem SnapDOM addresses with tiled rasterization. The key is not merely to split the final canvas. It is to move the cut earlier—before the browser decodes the image.
TL;DR: Capture the DOM once, move a tile-sized window across the resulting SVG, and rasterize each window into its own canvas. You keep the requested resolution without ever allocating a full-page bitmap.
The hidden limit behind “full-page screenshots”
A full-page capture involves two very different artifacts:
- A serialized SVG containing a styled DOM clone inside
<foreignObject>. - The bitmap created when the browser decodes that SVG for Canvas, PNG, JPEG, or WebP.
The SVG can describe a document tens of thousands of pixels tall. The bitmap is where browsers usually push back. The practical ceiling depends on the browser and the machine, but oversized images can make img.decode() fail with an unhelpful EncodingError, or cause canvas allocation to fail afterward.
For a normal export, downscaling is a sensible fallback. But it is the wrong trade-off when the pixels themselves are the product: deep-zoom viewers, page renderers, print pipelines, tiled uploads, or visual archives should not lose resolution just because one giant bitmap is a poor container.
Cut the SVG, not the canvas
The obvious mosaic algorithm does not solve the problem:
- Render one giant canvas.
- Use
drawImage()to cut it into smaller canvases.
By the time step two begins, the browser has already decoded and allocated the giant bitmap. The workaround arrives too late.
SnapDOM moves the crop earlier. With toCanvas({ crop }), SnapDOM rewrites the SVG root’s width, height, and viewBox so they describe only the requested window. Only then is the SVG passed to Image.decode().
The pipeline becomes:
Capture once → move the window → decode one small tile → consume it → repeat
The DOM is still cloned, styled, and serialized only once. Exporting ten tiles does not repeat font embedding, image inlining, or style collection ten times.
A practical tiling loop
The reliable crop bounds come from the capture metadata, not from reading the live element again.
async function tileCapture(capture, options, consume) {
const {
tileWidth = 2048,
tileHeight = 2048,
scale = 1,
dpr = 1
} = options
const { contentX, contentY, w0, h0 } = capture.meta
for (let y = 0, row = 0; y < h0; y += tileHeight, row++) {
for (let x = 0, col = 0; x < w0; x += tileWidth, col++) {
const width = Math.min(tileWidth, w0 - x)
const height = Math.min(tileHeight, h0 - y)
const crop = {
x: contentX + x,
y: contentY + y,
width,
height
}
const canvas = await capture.toCanvas({ crop, scale, dpr })
await consume({ canvas, crop, row, col })
}
}
}
const capture = await snapdom(document.documentElement, { dpr: 1 })
await tileCapture(capture, {}, async ({ canvas, row, col }) => {
const blob = await new Promise(resolve =>
canvas.toBlob(resolve, 'image/png')
)
if (!blob) throw new Error('PNG encoding failed')
try {
await uploadTile(blob, { row, col })
} finally {
canvas.width = canvas.height = 0
}
})
Exports on a single capture are queued, so the tiles rasterize in order instead of competing for decode and canvas memory. For genuinely large documents, encode, upload, or write each tile and release it before requesting the next one.
Geometry is part of the feature
Starting every mosaic at 0, 0 is not always correct. Root transforms, asymmetric shadows, outlines, and clipping can move the logical content inside the SVG viewBox.
SnapDOM exposes the final immutable geometry through result.meta:
| Property | What it measures | Typical use |
|---|---|---|
w0 / h0 |
Logical capture box | Tile only the page content |
contentX / contentY |
Content origin inside the viewBox | Position the first content tile |
vbW / vbH |
Complete serialized artifact | Include shadows, bleed, and outer effects |
Use contentX, contentY, w0, and h0 when you want the logical page. Use the complete viewBox when every pixel of transformed bleed or shadow must survive.
What tiling does—and does not—solve
Tiling removes the full-bitmap allocation from the equation. It does not turn DOM capture into a streaming renderer.
A few limits still matter:
- Lazy or virtualized sections must exist in the DOM before capture.
- Each tile, after
scale × dpr, must still fit the browser’s raster limits. - Keeping every tile alive can consume the same total number of pixels in aggregate.
- Stitching the tiles back into one giant canvas recreates the original problem.
- SnapDOM still clones the full subtree, reads its styles, inlines assets, and stores the serialized SVG.
- Each crop still parses the full
<foreignObject>payload.
If the initial DOM walk is the bottleneck, clip is the complementary tool: it prunes off-window subtrees before styling and inlining, but requires a separate capture for every region. By contrast, crop reuses one frozen artifact, keeping fonts, images, animations, and live data consistent across all tiles.
Where tile windows are useful
The original use case was document export, where a tall capture naturally becomes PDF pages. The same mechanism works well for:
- Deep-zoom or map-style viewers
- Poster and large-format printing
- Zoomable archives
- Multipart uploads
- Custom SnapDOM exporters and plugins
The claim is intentionally narrower than “unlimited screenshots.” One stable DOM snapshot becomes a sequence of bounded raster jobs. When every tile fits, the document no longer needs a global downscale.
Try the interactive mosaic
The original SnapDOM article includes a live demo that captures a 640 × 2,400 px report once and reconstructs it from ten independent canvases.
Open the interactive demo and full technical article →
You can also explore the Canvas crop API or view SnapDOM on GitHub.
Originally published by Zumerlab on the SnapDOM blog. This adapted version links back to the canonical article and interactive demo.
Comments (0)
Login to post a comment.