DEV Community

yue xing
yue xing

Posted on

DevTools said 2 MB of JS heap. The browser was holding 5 GB

I'm building a page for a personal project that compresses a whole folder of photos in the browser. Before picking a loop structure I wanted real numbers, so I generated 300 synthetic 3024×4032 JPEGs (a typical 12-megapixel phone size) and ran a few versions in desktop Chromium 149 on a 16 GB Mac. Every version does the same work per image: decode it with createImageBitmap, draw it on an OffscreenCanvas, re-encode it as JPEG at quality 0.8. The part that confused me was memory. Decoding 100 photos up front pushed the browser to 5,192 MB, and the JS heap peaked at 1.9 MB.

The JS heap stayed near 2 MB in every run

For each run I tracked two numbers: the memory of all the browser's processes added together, which is what the operating system sees, and the JS heap size, which is the figure DevTools shows. In this post MB means 1024×1024 bytes, the same as MiB in the chart.

Version Images Browser memory peak JS heap peak
Decode all first, then encode 30 1,531 MB 1.9 MB
Decode all first, then encode 100 5,192 MB 1.9 MB
One at a time, close() after each 300 111–192 MB while the loop ran 2.0 MB

The one-at-a-time row leaves out a single reading taken right at launch, before the first photo was decoded, because that is the browser starting up rather than the loop. If I had only watched DevTools, the decode-all version would have looked as harmless as the one-at-a-time loop. With 300 images it was already at 14,382 MB after 3.3 seconds, and I stopped it there because the machine only has 16 GB.

Peak browser memory when decoding 30, 100 and 300 photos up front, against the one-at-a-time loop

The bars are the decode-all runs. The dashed line is the highest reading of the one-at-a-time loop over all 300 photos once it got going, 192 MB. The bars grow with the image count while the line stays put, and the JS heap behind every one of them was about 2 MB. The chart comes from my own synthetic test images, no third-party material.

Where the missing gigabytes were

A decoded image costs width × height × 4 bytes, one byte each for red, green, blue and alpha. For 3024×4032 that's about 46.5 MB, and the decode-all bars do grow by 46 to 50 MB per image. The arithmetic was easy. What I hadn't understood is that none of it counts as heap. My current understanding is that the ImageBitmap object in JS is a small handle, and the pixels live in memory the browser manages itself, outside the V8 heap. I haven't worked out which process actually holds them, the page's renderer or the GPU process. My numbers just add every process together.

A console check you can repeat

To see it on a smaller scale I decoded 20 of the test photos at once, logged the heap, then closed them all:

const heapMB = () => (performance.memory.usedJSHeapSize / 1048576).toFixed(1);
async function holdAll(picked) {
  console.log(`before decoding: JS heap ${heapMB()} MB`);
  const bitmaps = await Promise.all(picked.map(f => createImageBitmap(f)));
  const pixelMB = bitmaps.reduce((sum, b) => sum + b.width * b.height * 4, 0) / 1048576;
  console.log(`holding ${bitmaps.length} bitmaps (${pixelMB.toFixed(0)} MB of RGBA pixels): JS heap ${heapMB()} MB`);
  return bitmaps;
}
function releaseAll(bitmaps) {
  bitmaps.forEach(b => b.close());
  console.log(`after close(): JS heap ${heapMB()} MB, first bitmap is now ${bitmaps[0].width}x${bitmaps[0].height}`);
}
Enter fullscreen mode Exit fullscreen mode
before decoding: JS heap 9.5 MB
holding 20 bitmaps (930 MB of RGBA pixels): JS heap 9.5 MB
after close(): JS heap 9.5 MB, first bitmap is now 0x0
Enter fullscreen mode Exit fullscreen mode

Between those steps I sampled the browser's total memory again. It was 57 MB idle, 990 MB while the page held the 20 bitmaps, and 58 MB about 1.5 seconds after close(). That 933 MB jump matches the 930 MB of pixels the snippet prints, and the heap line never moved. As far as I can tell Chrome rounds performance.memory in a normal launch, which is why it sits at a flat 9.5 MB while DevTools read about 1.8 MB. Two runs gave identical output.

What I watch now

For anything that decodes images I check the browser's task manager or the OS memory view, not the heap panel. I also keep only one decoded photo alive at a time: decode, draw, close(), encode, save, then the next file. That's the 111–192 MB row above, 300 photos in 24.6 seconds. My decode-all version also closed every bitmap, just at the very end, so calling close() wasn't the difference on its own. The peak followed how many decoded photos were alive at the same moment. I use ImgIng (https://imging.ai/) for my own photo batches, and its batch panel describes the same order: each photo is saved and released before the next one starts. I only measured Chromium 149 on one Mac, so I still don't know whether Firefox or Safari count decoded images the same way.

Top comments (0)