Every measurement argument ends the same way: put the two things on the scale and read the number. Photo: Kpubkalanami, Wikimedia Commons, CC BY-SA 4.0.
Five real photographic files, encoded across the whole ladder with the byte count and the pixel error recorded at every step. The default everybody ships is not the best row on the table.
Every gallery pipeline eventually asks the same question in the same lazy way: what JPEG quality should we use? Somebody says 85 because they read it somewhere, the line goes in, and nobody measures it again for two years.
So five real photographic images went through a full ladder this morning. JPEG at eight quality settings, WebP at three, PNG and lossless WebP for reference, with the byte count and the pixel error recorded at every step. The interesting result is not where JPEG stops being worth it. It is that WebP at quality 80 lands smaller than JPEG at 85 while sitting at almost the same error.
Five photographic images, each resized so the long edge is 1024 pixels, then encoded straight to memory. Nothing was written to disk, so filesystem caching never touched the byte counts. Every encoded file was decoded back and compared against the source pixels, and the error figure below is the root mean square difference across all three channels, on the usual 0 to 255 scale. Zero means byte identical pixels. Under 3 is invisible on a normal screen. Above 6 starts showing up in flat gradients and skin.
One caveat belongs at the top rather than buried at the bottom: the five sources were already JPEGs, so every lossy number here includes one generation of prior compression. That inflates the absolute error slightly at every setting. It does not change the ordering, which is what this test is for.
Mean across the five images, sorted by size.
| Encoding | Mean size | Mean RMSE | Versus JPEG 85 |
|---|---|---|---|
| PNG, optimize on | 1,183.0 KB | 0.00 | 628 percent larger |
| WebP lossless | 835.5 KB | 0.00 | 414 percent larger |
| JPEG q95 | 310.5 KB | 2.29 | 91 percent larger |
| JPEG q90 | 210.8 KB | 3.15 | 30 percent larger |
| WebP q90 | 177.8 KB | 2.65 | 9 percent larger |
| JPEG q85 | 162.4 KB | 3.80 | baseline |
| JPEG q80 | 134.3 KB | 4.30 | 17 percent smaller |
| JPEG q75 | 115.1 KB | 4.74 | 29 percent smaller |
| JPEG q70 | 102.9 KB | 5.08 | 37 percent smaller |
| WebP q80 | 100.9 KB | 3.94 | 38 percent smaller |
| JPEG q60 | 83.8 KB | 5.70 | 48 percent smaller |
| WebP q75 | 77.9 KB | 4.56 | 52 percent smaller |
| JPEG q50 | 71.4 KB | 6.19 | 56 percent smaller |
Compare the two bolded rows. JPEG at 85 costs 162.4 KB and produces an error of 3.80. WebP at 80 costs 100.9 KB and produces an error of 3.94.
That is 38 percent fewer bytes for an error difference of 0.14 on a 255 point scale. Nobody has ever seen 0.14. It does not survive the display panel, let alone the eye.
Now go one step further down the WebP ladder. WebP at 75 costs 77.9 KB with an error of 4.56, which is less error than JPEG at 70 while being 24 percent smaller than it. Across the whole middle of the curve the WebP line sits below and to the left of the JPEG line, which is the only shape that matters in a size versus quality plot.
The other number worth staring at is the top of the ladder. Going from JPEG 90 to JPEG 95 costs 47 percent more bytes, 210.8 KB up to 310.5, and buys an error improvement of 0.86.
That is the worst trade on the entire table. Half again the file size for a change nobody can see on a photograph. If a pipeline has quality 95 in it, that setting was almost certainly copied from a print workflow where it belonged, into a web workflow where it does not.
WebP q90 makes the point sharper still. It lands at 177.8 KB with an error of 2.65, which is smaller than JPEG 90 and less error than JPEG 90, and it is 43 percent smaller than JPEG 95 at an error only 0.36 worse. If somebody insists on a near transparent setting, that is the row to give them.
Two lossless rows are on the table because galleries keep shipping PNG for photographic content and calling it quality.
Optimized PNG averaged 1,183 KB. Lossless WebP averaged 835.5 KB, which is 29 percent smaller for pixel identical output. If a workflow genuinely requires lossless, lossless WebP is a straight upgrade over PNG on photographic material with no argument attached.
And the size of the gap to lossy is the real point. Lossless WebP is eight times the size of WebP q80 at an error difference of 3.94, which on a photograph is invisible. Shipping lossless to a browser is paying eight times the bandwidth for something the viewer cannot perceive.
The five images did not behave identically and the spread is worth knowing.
| Image | Pixels | JPEG q85 | WebP q80 | Saving |
|---|---|---|---|---|
| Portrait, tall crop | 683 x 1024 | 127.7 KB / 3.84 | 70.3 KB / 4.09 | 45 percent |
| Action, mixed background | 1024 x 974 | 175.7 KB / 3.43 | 92.3 KB / 3.82 | 47 percent |
| Wide scene, sky gradient | 1024 x 768 | 165.6 KB / 4.10 | 105.9 KB / 3.98 | 36 percent |
| Busy detail, high texture | 1023 x 1024 | 265.5 KB / 4.65 | 193.4 KB / 4.52 | 27 percent |
| Panorama, low detail | 1024 x 519 | 97.0 KB / 2.96 | 54.6 KB / 3.31 | 44 percent |
The saving ranges from 27 percent to 47 percent, and the pattern behind that range is legible. The busiest, most textured image saved the least, because high frequency detail is where WebP's advantage narrows. The two images with large smooth regions saved the most. On two of the five, WebP q80 produced less error than JPEG q85 as well as fewer bytes.
Serve WebP q80 as the default. It is 38 percent smaller than the JPEG 85 most pipelines ship, at an error difference of 0.14. On a gallery of a thousand images that is roughly 60 MB of bandwidth per full crawl.
Keep one JPEG fallback and generate it at 80, not 85. If a fallback exists purely for an ancient client, there is no reason for it to be the high quality copy.
Delete quality 95 wherever it appears. Forty seven percent more bytes than 90 for 0.86 of error is the worst trade on the table.
If lossless is a hard requirement, use lossless WebP rather than PNG. Twenty nine percent smaller, pixel identical, one argument change.
Open each source with Pillow, convert to RGB, call thumbnail((1024, 1024)), then save into a BytesIO at each setting and read the buffer length for the byte count. For the error figure, seek the buffer back to zero, reopen it, and take the channel histogram of ImageChops.difference against the source, then compute the root mean square from that histogram. Run every setting inside one process so the interpreter stays warm, and use method=6 on the WebP saves so the encoder is actually trying.
Three caveats stay on the record. Five images is a small sample and all five are photographic, so line art, flat colour and anime style output would move every number and could easily reverse the WebP advantage on the flattest material. The sources were already JPEG, which adds one generation of loss to every lossy row. And root mean square error is a crude proxy for perceived quality: it treats a small change in a smooth sky the same as a small change in an eyelash, and human eyes do not.