Skip to content

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

Description

@michal-baya

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):

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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions