HEIC to JPG on a slow connection: what the first load costs

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.

6 cold loads, 12 conversions 690,749 bytes before it works Chrome 154, Windows

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.

The first load at six speeds

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 paintedDecoder readyload eventUsableBytes from heicjpg.dev
No cap (measured 1,789,511 B/s)1,031 ms1,031 ms1,174 ms1,225 ms691,171
5 Mbit/s, 40 ms824 ms4,315 ms4,321 ms4,584 ms690,589
Fast 3G: 1.6 Mbit/s, 562.5 ms1,581 ms7,257 ms7,262 ms7,459 ms690,119
Slow 3G: 400 kbit/s, 2,000 ms4,992 ms22,419 ms22,427 ms22,526 ms690,749
100 kbit/s, 2,000 ms5,939 ms74,870 ms74,880 ms74,996 ms690,687
50 kbit/s, 2,000 ms7,090 ms145,248 ms145,259 ms145,348 ms690,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.

Why the box shows up long before it works

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.

What happens if you use the box during the dead window

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.

Check8 seconds after the dropAfter the decoder arrived (21,819 ms)
Spinnerhiddenhidden
Result areahiddenhidden
Error areahidden, emptyhidden, empty
File info lineemptyempty
Download linkno blobno blob
File still selected?yes, 1 fileyes, 1 file
Console messages / exceptions0 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.

The conversion itself does not care about your connection

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 speedConversion timeOutput
No cap1,820 ms38,078 B
5 Mbit/s3,110 ms38,078 B
Fast 3G2,472 ms38,078 B
Slow 3G834 ms38,078 B
100 kbit/s726 ms38,078 B
50 kbit/s744 ms38,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.

The second visit costs almost nothing, even at 50 kbit/s

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 speedFirst visitSecond visitBytes fetchedDecoder
Slow 3G: 400 kbit/s22,526 ms2,678 ms5,7740 — from cache
100 kbit/s74,996 ms2,779 ms5,7710 — from cache
50 kbit/s145,348 ms3,036 ms5,7810 — 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.

What a slow connection does not change

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.

If you are on a slow connection

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.

Questions

How long does the converter take to become usable on a slow connection?

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.

Why does the upload box appear so much earlier than it works?

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.

What happens if I drop a photo in before the page has finished loading?

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.

Is the JPG worse if my connection is slow?

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.

Is the second visit faster?

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.

Does it work with no connection at all?

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.

What I could not establish

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