#1918·rrweb

inlineImages encodes at full natural resolution, with no way to cap it

Author: michal-bayaCreated Sep 1, 2026Updated Sep 1, 2026

Affected: rrweb-snapshot 2.0.1, serializeElementNode, the img branch. Same code in the copy bundled by rrweb 2.0.0-alpha.17 — anchors below are from that bundle (dist/rrweb.js), where I could read them directly.

What the code does (dist/rrweb.js:897 onwards):

javascript
canvasService.width  = image.naturalWidth;
canvasService.height = image.naturalHeight;
canvasCtx.drawImage(image, 0, 0);
attributes.rr_dataURL = canvasService.toDataURL(
  dataURLOptions.type,
  dataURLOptions.quality
);

The canvas is sized to the image's natural dimensions with no cap, and toDataURL runs on the main thread. When dataURLOptions is unset both arguments are undefined, so the browser encodes a lossless PNG.

For a 2560x1920 source (4.9 Mpx) that is a large encode. I want to be explicit that I have not measured the resulting byte count on our own corpus — the figures usually quoted for lossless PNG at that pixel count are in the single-digit MB range before base64, but treat that as an order-of-magnitude expectation, not a measurement. What I did measure is the effect: our ingest endpoint, which accepts 10 MiB of decompressed JSON, answered 413 on these payloads.

Why this matters beyond size. The encode is synchronous, so on a page that brings several full-resolution images into the DOM at once it can block the main thread for a user-visible stretch. In our incident the gallery had already painted before the encoding ran, because the work is deferred to each image's load handler — note this is incident-specific, not guaranteed by the code: when image.complete is already true the branch encodes immediately instead (dist/rrweb.js:932).

Why the existing option is not enough. Within serializeElementNode's img/canvas paths, dataURLOptions reaches only toDataURL(type, quality). It never influences canvas sizing. Switching to image/webp q0.8 cuts the payload by a large factor and, together with a byte budget on our side, resolved the 413. It does not remove the synchronous encode of a 4.9 Mpx canvas, so the main-thread cost remains.

I could not find a public lever for this: recordOptions, plugins, hooks.mutation, packFn and eventProcessor all act on the finished payload (mutationCb at :11113, hooks.mutation wrapping it at :12259, eventProcessor/packFn just before emit at :13576) — none of them run before the canvas work.

Suggested fix. An option honoured before drawImage that caps the canvas — maxDimension or a pixel budget — scaling down while preserving aspect ratio. Separately, createImageBitmap plus OffscreenCanvas could move the draw and encode stages off the main thread, though bitmap acquisition and CORS handling would still touch it, and support would need a fallback.

Happy to send a PR if this shape is acceptable.