
My course project has a page where you pick an image, draw it onto a canvas, and export the result. When I fed it a huge test image, the preview came out blank. My first guess was memory: the image must be too big and the tab must be running out of RAM. So I spent an evening measuring how much memory a canvas actually takes. The numbers were interesting, and they also showed that my first guess was wrong.
How I measured
Everything ran in three engine builds: Chromium 149 as an open-source build rather than Chrome itself, a WebKit 26.5 build that is not Safari, and a Firefox 151 build that is not the release Firefox. The machine was an M4 Mac with 16 GB of RAM, and it was busy with other jobs at the time. All images were synthetic (gradients, shapes and noise generated by a script), not photos. For a reference point I also ran a 10000×10000 JPEG through the compress-and-convert page of ImgIng (https://imging.ai/), operating its Chinese UI, converting to WebP and JPG in all three engines; across 12 runs the network log showed zero non-GET requests.
The first thing I learned is that the browser's own tools can't see canvas memory. The DevTools heap metric JSHeapUsedSize stayed at 792,768 bytes whether the canvas was 1024² or 16384². performance.measureUserAgentSpecificMemory() was rejected with a SecurityError in the Chromium build even though the page was cross-origin isolated. So I measured from the operating system instead, summing the memory footprint of every browser process before and after each step.
Creating a canvas is free, drawing is not
Take one 8192×8192 run in Chromium 149 from 2026-09-28. The raw pixel data works out to 8192 × 8192 × 4 bytes, which is 256.0 MiB. Creating the canvas and calling getContext added 0.3 MiB. One full fillRect added 256.7 MiB, almost exactly the raw size. A full getImageData then took the total to 512.9 MiB. Over the same steps, JSHeapUsedSize only moved from 949,212 to 1,115,836 bytes. That was a separate run from the one above, so its baseline differs.
Just creating a canvas and calling getContext('2d') added less than 1 MiB in all three engines, all the way up to 16384×16384. Memory is allocated the first time you draw. After one full fillRect, Chromium and Firefox grew by exactly width × height × 4 bytes. For 16384×16384 that is 1025 MiB. Reading the whole thing back with getImageData doubled it to 2050 MiB, because the ImageData is a second full copy. WebKit's build went further and reached 3265 MiB after the readback at the same size. That is why I now avoid full-canvas getImageData unless I really need every pixel.
Decoding is its own cost. A 13.4 MB JPEG with 100 million pixels pushed the summed process peaks to 1289 MiB in Chromium, 1550 MiB in Firefox and 3525 MiB in WebKit. Those peaks are per-process maximums added together and may not happen at the same moment, so treat them as an upper-bound feel rather than exact usage. Still, a file size in megabytes tells you almost nothing about the memory it needs.
So was the blank preview a memory problem?
In my tests, no. I kept creating 4096×4096 canvases and keeping references to all of them. Before my script hit its own safety cap and stopped, the browsers had grown by 6826–7119 MiB with solid fills and by 3690–4041 MiB with noise, depending on the engine. No canvas went blank or lost its context along the way, and nothing crashed. I stopped there on purpose, so I can't tell you where the real limit is.
The blank preview came from somewhere else: the canvas size limit. In Chromium 149 and the WebKit 26.5 build, 16384×16384 draws fine but 16384×16385 silently draws nothing, and toBlob hands you null. My test image was bigger than that, and I never checked the callback value.
This is a small repro page I wrote, running in Chromium 149. I picked one ordinary image and drew it onto two canvases at once. On the left, the 4096×4096 canvas shows the picture, and Export PNG gives a 16.1 MB export.png. On the right, the only change is one extra row: 16384×16385. Inside the red box it is all checkerboard, so not a single pixel landed. The line under it says the exported file is 4 bytes. That is the null from the toBlob callback, wrapped into a file without a check. The page showed no error at any point.
If you want to see the memory side yourself, open Activity Monitor or Task Manager next to a test page, create one 8192×8192 canvas, then fill it, then call getImageData on all of it. Watch the number jump twice, and check toBlob for null before you blame memory.
Top comments (0)