The page does not go faster on a slow connection — it goes quiet. I loaded the live site six times today at six line speeds, from unthrottled down to 50 kbit/s, each time from a clean browser profile. The upload box painted in about a second every time, but the converter was not usable until the 680,033-byte decoder arrived: 4.3 seconds at 5 Mbit/s, 7.5 at Fast 3G, 22.5 at Slow 3G, 75 at 100 kbit/s and 145 at 50 kbit/s. In the gap between the box appearing and the decoder arriving, the box is completely dead — I dropped a photo into it at 4.7 seconds on a Slow 3G load and nothing happened at all: no spinner, no error, no console message, and no catch-up when the decoder finally landed 17 seconds later. Come back a second time and the same three slow connections cost 2.7 to 3.0 seconds, because the decoder is then already on disk.
Short answer: a slow connection delays the converter, it does not damage it. Every one of the 12 conversions I ran today produced the same file it always produces — 38,078 bytes for the 700 × 476 test photo and 3,177,680 for the 12-megapixel one — at every line speed, including 50 kbit/s. What changes is how long you wait before you can start, and how misleading the screen is while you wait.
The wait is dominated by one file. The first visit pulls about 881,800 bytes in total, of which 690,749 come from heicjpg.dev and the great majority of that is the decoder script — 680,033 bytes on the Slow 3G run. Until it lands, the converter does not exist in the page.
The upload box lies during that wait, politely. On the Slow 3G load the box was on screen at 4,992 ms and the decoder arrived at 22,419 ms — a 17.4-second window in which the drop zone looks ready and is not. At 50 kbit/s that window is 138 seconds.
The second visit is cheap even on a terrible line. With the decoder in cache, Slow 3G, 100 kbit/s and 50 kbit/s all became usable in 2.7 to 3.0 seconds, fetching 5,771 to 5,781 bytes. The slow-connection penalty on this page is paid once per browser, not once per photo.
How I measured it: today, in Chrome 154.0.8037.99 on Windows, against the live page, driving a real browser over the DevTools protocol with its network emulator. Six clean profiles — one per speed, each starting with an empty cache — plus three repeat visits and one dedicated experiment inside the dead window.
Each row is a separate browser profile with an empty cache, loading https://heicjpg.dev/ once. "Box painted" is when the upload area first exists in the page; "decoder ready" is when the conversion function is actually defined; "usable" is the wall-clock time from navigation to the page being complete and ready.
| Line speed (cap I set) | Box painted | Decoder ready | load event | Usable | Bytes from heicjpg.dev |
|---|---|---|---|---|---|
| No cap (measured 1,789,511 B/s) | 1,031 ms | 1,031 ms | 1,174 ms | 1,225 ms | 691,171 |
| 5 Mbit/s, 40 ms | 824 ms | 4,315 ms | 4,321 ms | 4,584 ms | 690,589 |
| Fast 3G: 1.6 Mbit/s, 562.5 ms | 1,581 ms | 7,257 ms | 7,262 ms | 7,459 ms | 690,119 |
| Slow 3G: 400 kbit/s, 2,000 ms | 4,992 ms | 22,419 ms | 22,427 ms | 22,526 ms | 690,749 |
| 100 kbit/s, 2,000 ms | 5,939 ms | 74,870 ms | 74,880 ms | 74,996 ms | 690,687 |
| 50 kbit/s, 2,000 ms | 7,090 ms | 145,248 ms | 145,259 ms | 145,348 ms | 690,678 |
The byte count barely moves between rows — between 690,119 and 691,171 from the site's own host, out of 881,249 to 882,301 including everything. A slow connection does not make the page smaller; it makes the same page take longer. Note also that the upload box arrives in about a second at every speed, and that box is the thing that looks like the product.
One honesty note about the speeds themselves. I measured what the emulator actually delivered for the decoder, and it under-delivered: 178,017 B/s against a 625,000 B/s cap at the 5 Mbit/s setting, 106,739 against 200,000 at Fast 3G, 33,873 against 50,000 at Slow 3G, 9,413 against 12,500 and 4,777 against 6,250. So the waits above are, if anything, worse than the nominal speed deserves, and you should read them as "this is what a Chrome-throttled load costs", not as a laboratory-grade model of a cell network.
This is not a design choice I have to guess about — it is visible in the page's own source. Today's home page is 13,924 bytes, and the decoder is loaded with a plain script tag near the very end of the document:
<script src="heic-to.js"></script> — the tag starts at byte 11,051, immediately followed by the inline script at byte 11,086 that wires up the drop zone, the file input and the result area. The upload box itself opens at byte 2,736, about a fifth of the way in.
There is no async and no defer on that script tag. So the browser parses and paints the upload box early, then stops at the bottom of the document and waits for 680,033 bytes to crawl in before it runs either the decoder or the code that makes the box clickable. Everything you can see is upstream of the wait; everything that matters is downstream of it. That is the whole mechanism behind every number in this article, and it is also why the fix is not obvious — making the script async would let the page finish parsing, but the box would still be inert until the file arrived, just without blocking the parser.
The page does have one honest signal: a status element holding the text Converting…, which the wiring script shows while a conversion runs. It is not shown while you are waiting for the decoder, because the code that would show it has not run yet either.
I ran this as its own experiment, on a clean profile at Slow 3G, because it is the situation a real person hits: the box is on screen, so you use it. The upload area appeared at 4,557 ms. At 4,663 ms I put a real 41,465-byte HEIC into the file input, and then watched.
| Check | 8 seconds after the drop | After the decoder arrived (21,819 ms) |
|---|---|---|
| Spinner | hidden | hidden |
| Result area | hidden | hidden |
| Error area | hidden, empty | hidden, empty |
| File info line | empty | empty |
| Download link | no blob | no blob |
| File still selected? | yes, 1 file | yes, 1 file |
| Console messages / exceptions | 0 and 0 | |
So: nothing happens, and nothing later makes up for it. The file sits selected in the input, the page never starts a conversion, and when the decoder finally arrives at 21,819 ms the page does not revisit the file that was already there — the state was still exactly the same after full load. I then selected a file again, and it converted normally: 38,078 bytes in 748 ms. The recovery is to pick the photo again, which is not something anyone would think to do, because nothing on screen suggested the first attempt failed.
Clicking the box does not help either. I clicked the centre of the drop zone during that same window and watched for the browser's file-chooser event: 0 of them. The file picker never opens, for the same reason the drop does nothing — the handler that calls the hidden input lives in the inline script that has not executed yet. From the outside, on a slow connection, the page looks broken for as long as it takes the decoder to arrive.
Once the decoder is in memory, the network is irrelevant — the work is local, as the network-layer measurement showed. Every cold run converted the same 700 × 476 photo after loading, and the times do not sort by line speed at all:
| Line speed | Conversion time | Output |
|---|---|---|
| No cap | 1,820 ms | 38,078 B |
| 5 Mbit/s | 3,110 ms | 38,078 B |
| Fast 3G | 2,472 ms | 38,078 B |
| Slow 3G | 834 ms | 38,078 B |
| 100 kbit/s | 726 ms | 38,078 B |
| 50 kbit/s | 744 ms | 38,078 B |
The two fastest conversions of the day came from the two slowest connections, which tells you the spread is warm-up noise rather than bandwidth. The same holds for a big file: the 12-megapixel photo (11,084,868 bytes, 4,032 × 3,024) took 9,152 ms unthrottled and 6,453 ms at Slow 3G, and both produced 3,177,680 bytes with the same SHA-256 hash. All 12 conversions today produced one of those two files, with no third variant — consistent with the byte-identical conversion measurement.
This is the part that makes the first table bearable. I re-used the profiles from three of the cold runs — so the decoder was already in Chrome's disk cache — and loaded the page again at the same throttled speeds:
| Line speed | First visit | Second visit | Bytes fetched | Decoder |
|---|---|---|---|---|
| Slow 3G: 400 kbit/s | 22,526 ms | 2,678 ms | 5,774 | 0 — from cache |
| 100 kbit/s | 74,996 ms | 2,779 ms | 5,771 | 0 — from cache |
| 50 kbit/s | 145,348 ms | 3,036 ms | 5,781 | 0 — from cache |
At 50 kbit/s the difference is 145,348 ms against 3,036 ms — about 48 times — for the identical page. The second-visit cost is essentially the HTML document plus one round trip of latency, and the upload box still appears at around 2.4 to 2.5 seconds. All three repeat visits converted the test photo afterwards, each to 38,078 bytes.
The catch is that "second visit" means "same browser, before the cache entry expires". The decoder is served with a four-hour freshness window, so a private window starts from zero every time — which is exactly what the private-window measurement found, 690,159 bytes re-downloaded per session.
Not the output. Same bytes, same dimensions, same hashes at every speed. If you can start a conversion at all, the line speed is not part of the result.
Not the layout. The upload box measured 288 px from the top of a 1,264 × 805 window in every run, throttled or not.
Not the third-party traffic. Every run fetched 191,130 bytes from www.googletagmanager.com and static.cloudflareinsights.com. In each of the five throttled runs, one request failed: the analytics beacon to /g/collect was aborted with net::ERR_ABORTED. It had no effect on any conversion.
Not the interruption behaviour. If the page does time out or you give up mid-load, the outcome matches the interrupted-conversion measurement: nothing is recoverable, and the only thing that survives is a download you already clicked.
Wait for the page to finish before you touch it. The reliable signal is not the upload box — that appears in about a second at any speed — it is the page's load completing, which on a slow line is tens of seconds later. If you picked a photo and nothing happened, pick it again once loading has finished; the first attempt is not queued and will not be retried.
If you convert more than one photo, do it in one session. The expensive part is the 680,033-byte decoder, and it is fetched once, not per photo. And if you are on metered data, a single visit costs roughly 882,000 bytes the first time and under 6,000 the next.
Measured from navigation: 4,584 ms at a 5 Mbit/s cap, 7,459 at Fast 3G, 22,526 at Slow 3G, 74,996 at 100 kbit/s and 145,348 at 50 kbit/s — all from empty caches. Unthrottled, 1,225 ms.
Because the decoder is a plain, blocking <script src="heic-to.js"></script> at byte 11,051 of a 13,924-byte document, after the upload box at byte 2,736. The box is painted long before the script that makes it work has been fetched.
Nothing at all. In my Slow 3G test the file stayed selected, the spinner never appeared, no error was shown and no console message was produced — and when the decoder arrived 17 seconds later it did not convert the file that was already there. You have to select it again.
No. All 12 conversions today produced either 38,078 bytes (700 × 476 photo) or 3,177,680 bytes (12-megapixel photo), with identical hashes, at every speed including 50 kbit/s.
Dramatically. Repeat visits at Slow 3G, 100 kbit/s and 50 kbit/s were usable in 2,678, 2,779 and 3,036 ms, fetching 5,771 to 5,781 bytes, because the decoder came from cache at 0 bytes.
Once the page has loaded, yes — the conversion is entirely local. That is covered in the offline measurement; what this page adds is the cost of getting to that state on a slow line.
Four things, plainly. These are Chrome's DevTools network emulator speeds, not a real cell network: latency here is a constant added per request, not the varying, lossy, handover-ridden thing a phone actually sees, and the emulator delivered less throughput than the cap I asked for at every setting, so the timings are pessimistic rather than flattering. I did not test what happens when a connection drops part-way through the first load, or stalls and resumes — only steady slowness. I tested one browser on one machine, so Safari and Firefox are unmeasured here, as are phones. And I did not test whether the page's behaviour differs on a genuinely large slow page-load queue, such as several tabs competing for the same slow line.
Try it: open the converter and watch how long the page takes before the drop zone actually responds — on a throttled connection, that gap is where the waiting happens.
First published: 11 October 2026