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.
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.
I drove a real Chrome against https://heicjpg.dev/ over the DevTools protocol and read two counters, because they answer different questions:
JSHeapUsedSize from Performance.getMetrics. This is the JavaScript objects the page itself holds.performance.memory.usedJSHeapSize, with --enable-precise-memory-info on. This includes the decoder and its buffers, which is where the big swings happen.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.
I pulled the page's own HTML and counted. It is 13,901 characters long:
| What I looked for | Occurrences |
|---|---|
createObjectURL — making a new address for a result | 1 |
revokeObjectURL — handing one back | 0 |
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.
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 run | Page's own heap | Renderer heap | DOM nodes | Event listeners |
|---|---|---|---|---|
| Before any conversion | 2,856,036 | 6,428,354 | 502 | 39 |
| After 50 conversions | 2,979,828 | 9,548,302 | 499 | 42 |
| After 100 conversions | 2,984,560 | 9,551,586 | 499 | 44 |
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.
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:
| Conversion | Page's own heap after it | What happened |
|---|---|---|
| #1 | 8,190,116 | — |
| #16 | 8,455,992 | highest reading of the run |
| #17 | 3,236,968 | browser collected — 5,219,024 bytes gone, DOM nodes 1,144 → 502 |
| #36 | 3,487,664 | climbed again |
| #37 | 3,226,212 | collected again — DOM nodes 597 → 502 |
| #50 | 3,405,112 | end 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.
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:
| Reading | Conversion #1 | Conversion #20 |
|---|---|---|
| Page's own heap, before | 2,790,436 | 2,869,756 |
| Page's own heap, after forced collection | 2,799,508 | 2,857,712 |
| Renderer heap, before | 9,353,696 | 9,433,210 |
| Renderer heap, right after | 69,229,642 | 69,284,835 |
| Renderer heap, after forced collection | 9,367,272 | 9,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.
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:
| Run | Results still readable | Total bytes still held |
|---|---|---|
| 100 small photos (700 × 476, 38,078 bytes each) | 100 of 100 | 3,807,800 |
| After a forced garbage collection in that same tab | 100 of 100 | 3,807,800 |
| 20 12-megapixel photos (3,177,680 bytes each) | 20 of 20 | 63,553,600 |
| After a forced garbage collection in that same tab | 20 of 20 | 63,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.
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.
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.
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.
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.
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.
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.
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