A review comment I keep seeing on image-upload PRs goes like this: the huge photo produces a blank export, so move the drawing into a Worker with OffscreenCanvas. It sounds reasonable. A Worker has its own event loop, so maybe it has its own budget too. I measured it, and the answer is no. The Worker changes where the work runs. It does not change how big a canvas can be.
I tested three engine builds on a 16 GB M4 Mac: the Chromium 149 open-source build (not Chrome itself), a WebKit 26.5 build and a Firefox 151 build. The WebKit build is not Safari, the Firefox build is not release Firefox, and I tested no phones. Every canvas was filled programmatically with solid color and marker pixels. For scale, 100 megapixels sits well inside every limit below. I cross-checked that tier with the converter on imging (https://imging.ai/), operating its Chinese-language UI: a 10000×10000 synthetic JPEG converted to WebP and JPG in all three engines, and the 12 cases in that run made zero non-GET requests.
Does OffscreenCanvas in a Worker have a bigger size limit?
I doubled each dimension until drawing failed, then bisected to the exact pixel, for three kinds of canvas: a <canvas> element, an OffscreenCanvas on the main thread, and an OffscreenCanvas inside a Worker. Every threshold matched across all three kinds, in all three engines. Chromium 149 and the WebKit build cap area at 268,435,456 pixels, so 16384×16384 draws and 16384×16385 does not. The Firefox build reaches 23168×23168. Single edges stop at 65,535 in Chromium and Firefox, and at 4,194,303 in WebKit.
Look at the bottom row first: "Identical to " in every column. Then compare the Firefox column with the other two, because that's the only place where the ceiling actually moves. It moves by engine, never by thread.
This is the Worker side of the check I ran for this post. It draws one pixel in the far corner and reads it back:
self.onmessage = ({ data: [w, h] }) => {
const el = new OffscreenCanvas(w, h), g2d = el.getContext('2d');
g2d.fillStyle = '#00f';
g2d.fillRect(w - 1, h - 1, 1, 1);
let acc = false;
try { acc = g2d.getImageData(w - 1, h - 1, 1, 1).data[3] === 255; } catch (e) {}
postMessage(acc);
};
With a height of 8, it returns true at width 65,535 and false at 65,536 in Chromium and Firefox. WebKit flips between 4,194,303 and 4,194,304. The same function on the main thread and on a <canvas> flipped at exactly the same widths. The try is there because the Firefox build throws NS_ERROR_FAILURE on the read instead of returning zeros.
This is a small repro page I wrote, open in the WebKit 26.5 build. Two canvases are 8 px tall, one 4,194,303 px wide and one 4,194,304 px wide. After Fill red, the page magnifies a slice from the left end, the middle and the right end of each. The top canvas is red everywhere. The bottom one is red at the left and in the middle, but the red box shows its last column at the right end is empty, with the checkerboard showing through. That is why the probe reads the far corner: a top-left check would call this width a success.
Why a thread can't buy you pixels
My reading is that the limit is attached to the bitmap allocation, not to the thread that asks for it. Creating a canvas and calling getContext without drawing added under 1 MiB in every engine. The memory shows up on the first draw: a filled 16384×16384 canvas cost exactly width × height × 4 bytes in Chromium and Firefox, 1025 MiB. Moving the call to another thread doesn't make that allocation any smaller. I didn't measure Worker memory separately, so I won't claim a Worker helps with memory pressure either.
What does change inside a Worker
The failure looks different. The <canvas> element in Chromium fires contextlost, and there is no element in a Worker to listen on. What you get is a rejected convertToBlob. Chromium rejects with IndexSizeError saying the OffscreenCanvas size is zero, while width and height still read back 16384×16385. WebKit rejects with EncodingError and Firefox with NS_ERROR_FAILURE. WebKit's "Canvas area exceeds the maximum limit" console warning barely reaches you from a Worker: 1 out of 104 over-area probes produced it. Don't wait for it.
This is the element side of the same failure, on the same repro page in Chromium 149. One sample.jpg is drawn onto two canvases. The 4,096 × 4,096 one shows the picture and exports a 16.1 MB PNG. The 16,384 × 16,385 one, a single row over the area cap, stays a blank checkerboard inside the red box, and nothing on the page reports an error. The 4-byte export.png is what happens when toBlob hands back null and the page wraps it in a File without checking: you get the string "null". In a Worker there is no element to look at, so the rejected convertToBlob is the only thing you will see.
So I keep the Worker for what it's good at, keeping the page responsive. Before posting dimensions to it, I check them against a per-engine table and downscale anything over. Inside it, I treat a rejected convertToBlob as "too big" and log the requested size next to the error text.



Top comments (0)