
Every few months someone tells me to serve big images as progressive JPEG. The pitch is always the same: the whole picture shows up blurry right away and sharpens as the bytes arrive, so it feels faster. I had never actually checked what I pay for that. So before the holiday I ran both kinds side by side on two images and wrote down three things: file size, decode time, and what each one can draw from a half-downloaded file.
Neither image is mine. One is a landscape photo from Wikimedia Commons, 5472×3648 originally, which I scaled to 2736×1824 to stand in for a typical hero image. The other is a Chinese National Day holiday notice template I found on Gaoding, a design template site, 1242×2688, mostly red with a card of text and a calendar grid. I saved each one at quality 75 and 85, once as baseline and once with progressive=True, using Pillow 11.3 (libjpeg-turbo underneath) with optimize=True on both. Decode time is the median of 7 runs of createImageBitmap in an open-source Chromium 149 build on an Apple M4 Mac with 16 GB of RAM.
What to look at here: grey is baseline, blue is progressive. On the left, file size, each blue bar is only slightly shorter than its grey neighbour. On the right, decode time, every blue bar is more than twice as tall. That lopsided picture is basically the whole answer.
In numbers, progressive came out 3.9% to 6.3% smaller at the same quality. The biggest saving was the template at quality 85, which went from 754,005 to 706,842 bytes. The photo at quality 85 went from 1,098,272 to 1,048,255 bytes. Decoding, though, took roughly 2.3 to 2.6 times as long. The photo at quality 85 took 11.1 ms as baseline and 28.3 ms as progressive. The template at quality 75 went from 6.4 ms to 15.6 ms. These are milliseconds. On a single hero image nobody will notice the difference. It starts to matter only on a page that decodes dozens of thumbnails at once, and even then I'd want to measure that page before believing it.
The part people actually care about is the blurry-first effect, and here I have to be careful. I tried to film it in the browser with a throttled connection, but every screenshot I took showed the image only after it had fully arrived. I got no in-between frames. So I can't say how many seconds progressive saves on a real page load. What I did instead was a decoder-level simulation. I kept only the first 10%, 25%, 50% and 75% of the photo's quality-85 files and let the decoder draw whatever it could. At 10% of the bytes the baseline file showed a thin strip at the top and grey below it. The progressive file showed the whole scene, soft but complete. PSNR against the original was 13.58 dB for baseline and 18.90 dB for progressive. At 50% progressive was at 30.97 dB and already hard to tell apart from the finished image, while baseline still only had the top half at 15.55 dB.
Then I checked what my own tools produce. In that Chromium build, canvas.toBlob('image/jpeg', 0.85) gives a SOF0 file, which is baseline. For everyday compression I use ImgIng (https://imging.ai/) in the same Chromium 149 on the M4, and the JPGs I had exported from it earlier are SOF0 as well. That makes sense, since its JPG path uses the browser's native encoder. If you want progressive, it has to happen somewhere else, on a server or in a local script with an encoder library like the Pillow flag above.
So is it worth it? For me, mostly no. My images get compressed in the browser and posted straight away, with no server step in between. Adding one just to save 4 to 6% doesn't pay off. If your upload pipeline already runs every image through Pillow or a similar library, it's one flag, and the decode cost is small unless the page shows a wall of thumbnails. One thing I haven't covered is Safari. Everything here ran on an open-source Chromium build, and I didn't test a shipping Safari.
If you're deciding, run file on a few of the JPEGs you already serve. It prints "baseline" or "progressive", and there's a good chance you'll find they're all baseline anyway.
Top comments (0)