deniz.in

Markets

Weather

Loading weather

· via dev.to (home feed)

Full-canvas getImageData doubles memory and Workers can't raise size limits

OS-level measurements across Chromium, WebKit and Firefox engine builds show a full-canvas read-back adds a second copy of the bitmap, and OffscreenCanvas in a Worker inherits the same per-engine size caps.

Full-canvas getImageData doubles memory and Workers can't raise size limits

What a read-back actually costs

Two dev.to posts published the same day report measurements taken on an Apple M4 with 16 GB of RAM against three engine builds: the Chromium 149 open-source build, a WebKit 26.5 build and a Firefox 151 build. Both authors stress that none of these are the browsers people actually download, so the figures describe engines rather than shipping Chrome, Safari or Firefox. Because nothing inside a page can observe canvas memory, the first post measured at the OS level by summing phys_footprint across every browser process of a launch.

The headline finding is that the familiar width × height × 4 estimate covers only the canvas itself. A full-canvas getImageData allocates a second buffer of the same size. At 16384×16384, roughly 268 megapixels, Chromium measured 1025 MiB after a fill and 2050 MiB after the read-back, with Firefox landing within about one percent of the same pair. The WebKit build was the outlier at 1206 MiB and 3265 MiB, and it also carried tens of MiB of overhead on small surfaces: a filled 1024×1024 cost 44.6 MiB where the formula predicts 4 MiB.

Timing details matter for anyone budgeting. Creating a canvas and calling getContext('2d') without drawing cost under 1 MiB in every engine, so the bitmap appears on first draw, not at creation. The JS heap is blind to all of it: during an 8192×8192 sequence in Chromium, OS-level memory climbed from 0.3 MiB to 256.7 MiB to 512.9 MiB while JSHeapUsedSize moved only from about 949 KB to about 1.1 MB. Zeroing the dimensions and dropping the reference did not return memory to the OS within 0.8 seconds in Chromium, which the author frames as a reminder that releasing memory and handing it back to the OS are separate moments, not evidence of a leak.

The Worker fix that isn't one

The second post targets a suggestion that keeps appearing in image-upload code reviews: when a huge photo produces a blank export, move the drawing into a Worker with OffscreenCanvas. The measurements say the thread makes no difference to capacity. The author doubled dimensions until drawing failed, then bisected to the exact pixel, for a canvas element, a main-thread OffscreenCanvas and an OffscreenCanvas inside a Worker. Every threshold matched across all three surface types in all three engines.

The caps themselves differ by engine. Chromium 149 and the WebKit build limit area to 268,435,456 pixels, so 16384×16384 draws while 16384×16385 fails; the Firefox build reaches 23168×23168. Maximum single edges stop at 65,535 pixels in the Chromium and Firefox builds and at 4,194,303 in WebKit. The ceiling moves by engine, never by thread, and the author's interpretation is that the limit attaches to the bitmap allocation rather than to whoever requests it.

How over-limit canvases fail

The failure modes are where threads do change things. On the page, an over-limit canvas in Chromium simply rendered as a blank checkerboard with no error reported, and when toBlob handed back null an unchecked wrapper produced a 4-byte PNG containing the text "null". Over-limit canvases can also render partially, with the far end empty while the left and middle look correct, which is why the author's verification probe reads a corner pixel instead of the top-left.

Inside a Worker there is no element to fire contextlost, so a rejected convertToBlob is the only visible signal. Chromium rejects with IndexSizeError claiming the size is zero even though width and height still report the requested values; WebKit rejects with EncodingError and Firefox with NS_ERROR_FAILURE, the latter also thrown on over-limit getImageData reads instead of returning zeros. WebKit's console warning about exceeding the canvas area limit surfaced in only one of 104 over-area probes from a Worker.

Practical takeaways

The budget the author now uses is pixels × 4 bytes, doubled whenever anything reads the pixels back. That matched Chromium and Firefox within about one percent, but WebKit exceeded it — 823.3 MiB after read-back at 8192×8192 against a predicted 512 — so the ceiling is set below the formula rather than guessing a per-engine multiplier. Dimensions are checked against a per-engine table and downscaled before being posted to a Worker, and a rejected convertToBlob is treated as "too big" with the requested size logged next to the error.

The authors list clear limits: no release browsers were tested, no phones, GPU-backed canvases were not covered, no crash threshold was established because testing stopped at safety limits on a busy machine, and Worker memory was not measured separately.

Why it matters

Image pipelines tend to hold two assumptions that these measurements contradict: that a canvas costs width × height × 4, and that a Worker is an escape hatch for oversized images. The first under-counts by a full copy whenever getImageData runs, and the second changes the failure mode without changing capacity. The resulting bugs are silent — blank exports and null blobs rather than thrown errors — and invisible to page-level heap metrics. The reliable checks are OS-level memory measurement and probing a rendered corner pixel to confirm the draw actually happened, in the browsers you actually ship to.

  • #canvas
  • #memory
  • #offscreen-canvas
  • #web-workers
  • #browser-engines