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.
Affected:
rrweb-snapshot2.0.1,serializeElementNode, theimgbranch.Same code in the copy bundled by
rrweb2.0.0-alpha.17 — anchors below are fromthat bundle (
dist/rrweb.js), where I could read them directly.What the code does (
dist/rrweb.js:897onwards):The canvas is sized to the image's natural dimensions with no cap, and
toDataURLruns on the main thread. WhendataURLOptionsis unset botharguments 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
413on thesepayloads.
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
loadhandler — note this is incident-specific, not guaranteed by the code:when
image.completeis already true the branch encodes immediately instead(
dist/rrweb.js:932).Why the existing option is not enough. Within
serializeElementNode'simg/canvas paths,
dataURLOptionsreaches onlytoDataURL(type, quality). Itnever influences canvas sizing. Switching to
image/webpq0.8 cuts the payloadby 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 themain-thread cost remains.
I could not find a public lever for this:
recordOptions, plugins,hooks.mutation,packFnandeventProcessorall act on the finished payload(
mutationCbat:11113,hooks.mutationwrapping it at:12259,eventProcessor/packFnjust before emit at:13576) — none of them runbefore the canvas work.
Suggested fix. An option honoured before
drawImagethat caps the canvas —maxDimensionor a pixel budget — scaling down while preserving aspect ratio.Separately,
createImageBitmapplusOffscreenCanvascould move the draw andencode 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.