Skip to content
SnapDOMGitHub★ 8K
SnapDOM Blog

Tiled rasterization for large DOM captures

Capture a document once, then rasterize it through small SVG windows. The crop is applied before image decode, so each tile fits the browser's raster limits.

For a single large PNG or JPEG file, use download() or toBlob(): SnapDOM assembles the file automatically. Use the crop API below when your output needs separate tiles or pages.

TL;DR

A very tall DOM capture can serialize to SVG and still fail when the browser decodes that SVG into one enormous bitmap. toCanvas({ crop }) windows the SVG before image decode. Capture once, move the window across result.meta, and each tile keeps the requested resolution while fitting the browser's raster limits, with no full-page canvas ever allocated. It solves the raster ceiling; it does not make cloning an enormous DOM free.

The screenshot was fine. The bitmap wasn't.

A “full-page screenshot” has two sizes. One is the serialized capture: an SVG with a styled DOM clone inside <foreignObject>. The other is the bitmap the browser creates when that SVG is decoded for PNG, Canvas, JPEG or WebP.

The SVG can describe a page tens of thousands of pixels tall. The bitmap is where browsers push back. SnapDOM budgets 16,384 pixels per side on WebKit, 32,767 elsewhere, and about 268 million pixels of area (16,384 × 16,384). Actual allocation and decode limits depend on the browser and device. dpr counts toward the limit and defaults to devicePixelRatio, so on a Retina screen a scale: 2 capture is 4× wide and tall. Past the limit, img.decode() may reject with EncodingError: The source image cannot be decoded, or the canvas allocation fails later.

Canvas-sized exports are proportionally downscaled when they exceed these budgets. For a deep-zoom viewer, tiled upload or print pipeline, separate crop windows preserve the requested resolution without a full-size canvas.

Cut the SVG, not the canvas

The obvious mosaic algorithm is the broken one: render one giant canvas, then cut it into smaller canvases with drawImage(). By the time the slicing loop starts, the browser has already decoded and allocated the giant bitmap.

toCanvas({ crop }) moves the cut earlier. SnapDOM rewrites the serialized SVG's root width, height and viewBox to describe only the requested window, then hands it to Image.decode(). The browser never sees a full-height raster source.

Capture onceClone, style and serialize one canonical SVG.
Move the windowRewrite the viewBox to one tile's coordinates.
Decode smallRasterize only that tile, consume it, repeat.

All of this lives in the exporter. The capture path still walks and clones the DOM once; ten tiles do not repeat style collection, font embedding or image inlining ten times.

Try the mosaic

The report below is 640 × 2,400 CSS pixels, well under any browser limit so the demo stays polite, but it takes the same path as a 40,000-pixel document: one SVG capture, ten 320 × 480 crop windows, ten independent canvases. Scroll the source, then build the mosaic.

Northstar / Annual report 2026

Revenue, retention and regional growth.

One continuous DOM document: metrics, charts, regions and a timeline. The mosaic below never rasterizes it as one continuous bitmap.

Annual revenue$18.4m↑ 24.8%
Active teams12,840↑ 18.1%
Net retention117%↑ 6.2 pts
Performance

Recurring revenue by quarter

Where growth came from

Four regions, four different jobs

North America

Enterprise expansion made existing accounts the largest contributor to net new revenue.

Europe

Localized onboarding shortened time-to-value across the mid-market segment.

Asia Pacific

Partner-led launches opened three markets without adding a regional sales layer.

Latin America

Self-serve adoption doubled after local currency pricing reached the checkout.

The year in four moves

Four releases that moved adoption

New workspace model

Teams could finally organize projects without duplicating permissions and billing.

Usage-based plans

Smaller customers could start earlier and grow without a contract migration.

Regional data storage

European deployments moved from exception handling to the standard path.

Partner API

Implementation partners shipped repeatable integrations instead of one-off scripts.

Priorities for 2027

Faster onboarding, regional growth and durable retention.

One capture · no full-page canvas

The whole implementation is a loop

crop uses SVG viewBox coordinates, so the reliable bounds come from the capture, not from rereading the live element. Each canvas goes to a callback right away, so a production exporter can encode, upload or write the tile before requesting the next one.

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 one capture are queued, so tiles rasterize in order instead of competing for decode and canvas memory. A visual mosaic like the demo can keep every canvas. For a huge page, process a tile and release it before moving on.

The geometry is half the feature

A naïve tiler starts at 0, 0, which only works while the serialized content starts there too. Root transforms, asymmetric shadows, outlines and clip windows can move the logical page inside the SVG viewBox. SnapDOM exposes the final, immutable geometry as result.meta, after those effects are resolved.

GeometryWhat it measuresUse it for
w0 / h0The logical capture boxTile only page content
contentX / contentYIts exact origin inside the viewBoxPlace the first content tile
vbW / vbHThe complete serialized artifactInclude bleed and outer effects

The loop above tiles the logical page from contentX, contentY over w0 × h0. To keep every pixel of shadow or transformed bleed, tile from 0, 0 over vbW × vbH instead. Crop windows partly outside are clipped to the viewBox; empty, non-finite or fully outside windows reject rather than quietly returning the whole image.

What tiling does not solve

Tiling removes the full-bitmap allocation. It does not make capture a streaming DOM renderer: SnapDOM still clones the complete subtree, reads its styles, inlines assets and holds the serialized SVG. A 100,000-node document is still a 100,000-node capture.

  • Virtualized or lazy sections must exist in the DOM before capture; no image library can serialize nodes the application has not rendered.
  • tileWidth × scale × dpr and tileHeight × scale × dpr still need to fit the browser's raster limits.
  • Keeping every tile alive holds the same pixels in aggregate. Encode or send each tile as you go when memory matters.
  • Stitching the tiles back into one giant canvas recreates the original problem. The consumer has to understand pages, tiles or levels.

If the initial DOM walk is the bottleneck, clip is the other tool: it prunes off-window subtrees before styling and inlining, but needs a separate capture per region. crop trades that pruning for one frozen artifact, so fonts, images, animation state and live data cannot change between tiles.

Each crop also decodes and re-encodes the complete SVG data URL, and the browser still parses the full <foreignObject> payload. Tiling bounds bitmap allocation only.

Where tile windows are useful

We added crop and immutable geometry for document exporters, where a tall capture becomes PDF pages. The same mechanism fits map-style viewers, poster printing, zoomable archives and multipart uploads: one stable source, one rectangle at a time.

In defineExports(ctx), plugin authors can retain ctx.exports and call its canvas({ crop }) method from their custom exporter. This core-export facade reuses the canonical capture without recursively firing export hooks, so “mosaic” is a small custom export rather than a second rendering engine, and the extra work stays out of SnapDOM's capture hot path.

toCanvas({ crop }) and immutable capture geometry have been available since SnapDOM v2.24. See the crop reference.