DEV Community

NullPointerZen
NullPointerZen

Posted on

Canvas memory starts at draw

I build small internal tools for a content team, and one of them stitches large images on a canvas. When someone asked how big an image it could take before the tab fell over, I realized I had no idea what a canvas actually costs in memory. So I measured it in three engine builds: the Chromium 149 open-source build (not Chrome itself), a WebKit 26.5 build, which is not Safari, and a Firefox 151 build that isn't release Firefox either. Treat every number here as belonging to those builds only. The machine was an M4 Mac with 16 GB of RAM that was busy with other jobs at the same time. All images were synthetic ones I generated with a script, not photos.

How I measured canvas memory

Browser APIs were no help here. performance.measureUserAgentSpecificMemory refused to run in the Chromium build even with cross-origin isolation on, and the other two builds don't have it. The JS heap size shown in DevTools barely moved while a gigabyte-sized canvas sat in memory. So I went one level down and summed phys_footprint from macOS footprint for every process the browser launched, then subtracted the idle page. It's a blunt number, but it's the only one that actually saw the canvas. For a reference point on real in-browser processing I used ImgIng (https://imging.ai/) with a 10000×10000 synthetic JPEG, operating the Chinese-language interface in all three engines. Across its 12 test runs the network log showed zero non-GET requests.

Creating a canvas costs almost nothing

Setting width and height and calling getContext('2d') added less than 1 MiB in all three engines, even for large sizes. Memory shows up on the first draw. After one full-size fillRect, Chromium and Firefox grew by exactly width × height × 4 bytes: a 16384×16384 canvas added 1025 MiB. Reading the whole thing back with getImageData added another copy of the same size, which took Chromium to 2050 MiB above idle. WebKit had its own overhead on top: the same 16384×16384 canvas reached 3265 MiB after the full readback. If you pull pixels out of a big canvas just to loop over them, you are paying for the image twice.

Memory increase against canvas size in millions of pixels, two lines per engine: after one full fill and after a full getImageData. Read the points at 268 million pixels (16384×16384): Chromium and Firefox overlap at about 1025 MiB after the fill and about 2050 MiB after the readback, while WebKit's readback line climbs to 3265 MiB

Decoding a big JPEG costs more than the file

A 13.4 MB JPEG at 100 megapixels looks harmless on disk. Decoding it pushed the summed process peaks to 1289 MiB in Chromium, 1550 MiB in Firefox and 3525 MiB in WebKit. Those are peaks added together across processes, and they don't necessarily happen at the same moment, so they overstate how much is held at once.

Many canvases in a row

I kept creating 4096×4096 canvases and holding on to all of them. None of the three builds crashed or lost a canvas before my own safety stop at 7 GiB with solid fills, or 4 GiB with noise. I stopped there on purpose because the machine only has 16 GB, so I still don't know where the real ceiling is. One thing surprised me: with solid colors the system's free memory barely dropped. My guess is macOS memory compression, but I didn't capture the compressor numbers, so that stays a guess.

What I changed in my tool

I budget memory per image as width × height × 4 before drawing anything, then double it if the code needs getImageData on the full canvas. For anything near 100 megapixels I check that budget first and scale down when it doesn't fit. I also stopped reading back whole canvases when I only need a few pixels.

Top comments (0)