DEV Community

hao jia
hao jia

Posted on

Same 150 KB, three formats: what you actually get

Someone on my team opened a PR that moved our export quality from JPEG 80 to WebP 80, with a note saying "same quality, smaller files". I didn't merge it. Those two 80s are not the same ruler, and the benchmark behind the claim was measuring the wrong thing.

Our constraint is a page budget: hero images on the product detail page must stay under 150 KB each. The budget is written in bytes. So the question is not "which format is smaller at the same quality" — it's "at the same 150 KB, which one buys me the most image quality". Those are two different experiments with two different answers.

Why quality 80 is not quality 80

JPEG's quality scales a quantization table. WebP's quality drives a whole group of prediction-search and quantization parameters. AVIF's quality gets mapped into an AV1 qindex. No standard ties these three scales to the same distortion level. Setting all three to 80 and comparing file sizes tells you one thing only: what those three unrelated rulers read at their own 80 mark.

Think of each format as a rate-distortion curve. To compare curves you have to slice along an axis. Slice along quality and you learn "how many bytes to reach the same fidelity". Slice along size and you learn "how much fidelity you get for the same bytes". Comparing q80 to q80 picks one arbitrary point on each curve and leaves both variables moving.

Pinning the bytes

For each format, binary search quality 1..100 for the highest value that still fits the budget:

from bisect import bisect_right

class SizeAt(list):
    """len(self) == 101; self[q] encodes at that quality and returns the byte size"""
    def __init__(self, im, fmt): self.im, self.fmt = im, fmt; super().__init__(range(101))
    def __getitem__(self, q):
        buf = io.BytesIO(); self.im.save(buf, self.fmt, quality=q); return buf.tell()

def best_quality(im, fmt, budget):
    return bisect_right(SizeAt(im, fmt), budget) - 1
Enter fullscreen mode Exit fullscreen mode

This assumes size grows monotonically with quality. It usually does, but that's an observation, not a theorem — I swept the whole range once to confirm there was no inversion before trusting the search.

The sample is a CC0 photo from Wikimedia Commons (shot on an iPhone 6, 2048×1536, 638.7 KB), not my own material. Lots of sky and metal reflections, so it is on the hard side to compress. Budget: 150 KB.

Format Quality that fits Size PSNR SSIM Encode time
JPEG q23 147.6 KB 33.7 0.938 4 ms
WebP q65 149.8 KB 37.6 0.965 126 ms
AVIF q59 145.8 KB 38.9 0.973 131 ms

Same photo squeezed to roughly 150 KB in three formats, with the quality setting each one needed

The interesting column is the second one. To fit 150 KB, JPEG has to fall all the way to q23 — zoom into the sky and you can see blocking. WebP stops at q65, AVIF at q59. That leftover quality headroom is exactly where the 5.2 dB spread comes from. With a q80-vs-q80 table, that column never appears.

What I actually changed

Budget first, format second. We now derive a per-image byte ceiling from the page budget, then pick the format by measured fidelity at that ceiling. Quality is a means to hit the budget, not a number you compare across formats.

Encoding time is a real cost. 4 ms vs 126 ms vs 131 ms is a thirty-fold spread. That number only describes one Pillow call on my Mac, but the direction holds: the bytes you save on the wire get paid for on the encoder. Our upload pipeline fans one original out into five sizes, so switching the primary format means re-running load tests, not flipping a constant. I did not test AVIF decode support on older devices in this round, so the JPEG fallback in <picture> stays.

Budgets belong per content type. The same run included a 1280×800 UI screenshot: 94.0 KB in, 31.6 KB out as WebP q80, 18.5 KB as AVIF q60, both above 40 dB. Flat color and sharp text compress far better than photos, so one global target wastes headroom.

Soft-edged and semi-transparent assets get their own path. Badges and shadowed overlays lose their gradient alpha when you push them through palette reduction. The shadowed transparent badge in that run went from 35.2 KB to 20.2 KB as WebP and 2.4 KB as AVIF, with the alpha levels intact.

One last constraint that isn't technical. These were unreleased product assets, and anything leaving our network needs a compliance sign-off — we spent three months under legal review after a data remediation two years ago, and I've asked that question first ever since. So besides the local script I ran the same comparison in the browser with Imging, where compression and conversion for common formats happen locally without an upload. The numbers lined up with my script, and I didn't have to file a request to check a table.

If you want the version of this that applies to your images: take your largest hero image, pick a byte ceiling, binary search each format, then open the three outputs side by side at 200% and look at a gradient area.

https://imging.ai/

Top comments (0)