Converting 50 photos in a row: what happens to browser memory

The page's own memory barely moves over a long session. What piles up is the finished JPGs themselves — the tab keeps every one of them until you reload it.

100 conversions: +128,524 bytes All 100 results still in memory 63,553,600 bytes never given back

Short answer: the page's own memory does not run away. After 100 conversions in one tab, a forced garbage collection left the JavaScript heap at 2,984,560 bytes against a 2,856,036-byte starting floor — 128,524 bytes more, and the second fifty added only 4,732 bytes on top of the first fifty's 123,792. What does grow without limit is your finished photos. The page makes a fresh object URL for every result and never hands one back, so all 100 results were still in memory at the end — 3,807,800 bytes of them — and forcing a collection did not release a single one.

Measured today on this site in Chrome 153: twenty 12-megapixel photos in a row left 63,553,600 bytes of finished JPGs in the tab, all 20 still readable after I forced a garbage collection. Each 12-megapixel conversion lifted the renderer's heap by 59,875,946 bytes while it ran and dropped back when the collection happened; left to the browser's own timing, the highest reading I saw was 189,371,691 bytes.

Reloading is the only thing I measured that gives any of it back.

How I measured it

I drove a real Chrome against https://heicjpg.dev/ over the DevTools protocol and read two counters, because they answer different questions:

Alongside them I recorded DOM node counts and event-listener counts. For a second set of runs I also forced a full collection through HeapProfiler.collectGarbage before reading, which gives a floor — what is genuinely being held, as opposed to garbage that simply has not been swept yet. Every number in this article is one of those readings.

Two files, at the default 0.85 quality: a 700 × 476 photo of 41,465 bytes that comes out at 38,078 bytes, and a 4032 × 3024 photo of 11,084,868 bytes that comes out at 3,177,680 bytes. The browser was headless Chrome 153. That is a caveat worth stating: a headless browser may run its collections at different moments than a visible, busy tab, so treat the timings as this machine's, and note that I did not measure a phone browser at all.

One line of source explains most of it

I pulled the page's own HTML and counted. It is 13,901 characters long:

What I looked forOccurrences
createObjectURL — making a new address for a result1
revokeObjectURL — handing one back0

That is the whole story in two counts. Every conversion mints a new address and nothing ever unmints one, so a result stays reachable for as long as the tab lives. You can see the same thing from the other direction in the two-files-at-once measurements, where a result replaced by a later conversion was still fetchable afterwards.

100 small photos in one tab

Fifty conversions, then fifty more, in the same tab, with no reload in between. All 100 succeeded and every one produced 38,078 bytes. Here are the floors — readings taken right after a forced collection:

Point in the runPage's own heapRenderer heapDOM nodesEvent listeners
Before any conversion2,856,0366,428,35450239
After 50 conversions2,979,8289,548,30249942
After 100 conversions2,984,5609,551,58649944

So: 123,792 bytes of extra heap after the first fifty, and 4,732 more after the second. The DOM node count came back to 499, below where it started, and the listener count went from 39 to 44. There is no runaway here — a hundred photos cost the page about a tenth of one of its own results.

Read without forcing a collection, the same fifty conversions look less tidy: 2,935,536 bytes after the first photo rising to 3,158,348 after the fiftieth, a climb of 222,812 bytes. Almost all of that is unswept garbage, which is why the floor above is the honest figure.

The sawtooth when the browser collects on its own

In an earlier run of fifty I never forced anything and just watched. The page's heap climbed, hit a ceiling, dropped hard, climbed again:

ConversionPage's own heap after itWhat happened
#18,190,116—
#168,455,992highest reading of the run
#173,236,968browser collected — 5,219,024 bytes gone, DOM nodes 1,144 → 502
#363,487,664climbed again
#373,226,212collected again — DOM nodes 597 → 502
#503,405,112end of run

Over the same run the renderer heap swung between 11,234,716 and 37,316,462 bytes. The shape is the useful part: memory rises steadily, then falls off a cliff when the browser decides to sweep, and the cliff gets lower each time. Nothing in the page's own bookkeeping survives that sweep in any meaningful quantity.

12-megapixel photos are a different scale

A 4032 × 3024 photo carries far more pixels than the small one, and the temporary buffers scale with it. With a forced collection after every single conversion, the pattern was perfectly repeatable:

ReadingConversion #1Conversion #20
Page's own heap, before2,790,4362,869,756
Page's own heap, after forced collection2,799,5082,857,712
Renderer heap, before9,353,6969,433,210
Renderer heap, right after69,229,64269,284,835
Renderer heap, after forced collection9,367,2729,424,748

The renderer step is 59,875,946 bytes for one 12-megapixel conversion, and it is entirely temporary: a forced collection took it back to 9,367,272 after the first conversion and 9,424,748 after the twentieth — both back at the level they started from. Over those twenty conversions the page's own heap floor crept from 2,799,508 to 2,857,712, a rise of 58,204 bytes, which is real but small.

Left to the browser's own timing, those temporary buffers stack up instead of being swept immediately. In a run where I forced nothing, the readings taken after successive conversions went 69,164,013, then 129,029,970, then 188,902,875, and the highest in that run was 188,946,070. Across every run today the highest single reading was 189,371,691 bytes. I did not establish what those unswept buffers consist of, so I will not attribute them to any particular stage of the conversion.

For scale, the heap ceiling Chrome reported was 2,214,330,368 bytes.

The results pile up, and a collection does not clear them

This is the part that matters, and it is the reason the page's own heap can look fine while the tab is quietly holding everything you have made. I kept the address of every result, finished the run, and then fetched each one:

RunResults still readableTotal bytes still held
100 small photos (700 × 476, 38,078 bytes each)100 of 1003,807,800
After a forced garbage collection in that same tab100 of 1003,807,800
20 12-megapixel photos (3,177,680 bytes each)20 of 2063,553,600
After a forced garbage collection in that same tab20 of 2063,553,600

Not one byte came back. A garbage collection can only free what nothing can reach, and every one of these results is still reachable — the page is holding the address. Twenty phone photos is 63,553,600 bytes; a hundred is the same arithmetic again. I did not run a hundred 12-megapixel conversions, so I will not put a number on that case.

Reloading is what actually clears it. After twenty 12-megapixel conversions the tab was at 2,866,200 bytes of page heap and 9,429,609 of renderer heap; a reload followed by a forced collection gave 2,792,180 and 9,359,376, both below the post-run figures and back at the level of a fresh page. The reload was ready in 523 ms. Reloading also destroys the results themselves — a finished JPG does not survive it, which is measured separately in the reload and tab-switch tests.

Speed did not degrade

Worth checking, because a slow leak usually shows up as a slowdown first. It did not:

The twentieth photo was not slower than the second. If you are converting a long batch one at a time, the per-photo time is not the thing that will hurt you; the accumulated results are. The overall time for a large batch is measured in the 200-file timing run.

What to do

Questions

Does the tab eventually crash if I convert enough photos?

I did not push it to failure, so I will not claim either way. What I can say: the highest renderer reading I saw was 189,371,691 bytes against a reported ceiling of 2,214,330,368, and the page's own heap, read after a forced collection, never went above 2,984,560 bytes. The thing that grows without bound is the finished results, and I stopped at 100 small ones and 20 large ones.

Why doesn't a garbage collection free the finished JPGs?

Because nothing has been released. The page calls createObjectURL once per result and revokeObjectURL zero times in its entire 13,901 characters of source. A collection can only reclaim what is unreachable, and these are all still reachable.

Is the output different on the hundredth conversion?

No. Every one of the 100 came out at 38,078 bytes and every one of the 20 large ones at 3,177,680 bytes, matching the repeat-conversion figures.

Does closing the tab release it?

I did not measure what the operating system gets back when a tab closes, so I will not say. Reloading is what I measured, and it returns the tab to a fresh-page level.

Is the 59,875,946-byte step the size of the photo?

It is larger than the photo and larger than its output — the input is 11,084,868 bytes and the JPG is 3,177,680. I did not break the step down into decoder buffers, pixel arrays and output, so I will not attribute it.

Where does the decoder itself fit in?

It is fetched once and cached, so a reload costs 0 bytes of fresh download — measured in the reload tests, and the file itself is described in what the decoder is.

Try it: open the converter, convert a photo, then convert a second one and watch the result line change — the first JPG is still in the tab, waiting for a reload.

First published: 5 October 2026