JPEG vs WebP at the same file size
By Sucheet BoppanaPublished Updated
On the photograph-like sample the two formats really did land on the same bytes, and WebP won: 41.22 dB PSNR and 0.9797 SSIM at the 100 KB ceiling against JPEG's 36.98 and 0.9568, and 36.92 and 0.9566 at 50 KB — within 0.06 dB of what the JPEG needed 96.97 KB to reach. The screenshot, the graphic and the illustration gave no equal-size comparison at all, because a byte target is a ceiling and all three sources were already beneath it: at the 100 KB ceiling WebP returned 23.98 KB, 62 KB and 16.48 KB while JPEG raised its quality to between 89 and 100 and filled the ceiling with 94.42 KB, 94.15 KB and 48.41 KB. WebP scored higher on all eight pairings, from 1.95 dB at the narrowest to 9.34 dB at the widest, but only the two photograph rows compare two files of the same size. The samples are drawn rather than photographed, the run is one laptop and one build, and every figure comes from driving the real pages in Chromium with nothing uploaded.
Methodology
Sixteen cases: four samples, two byte ceilings and two output formats. Each one drives the real pages in Chromium — the file goes through Convert to reach the output format, then through Compress with the ceiling typed into the target field — and the downloaded file is what gets measured. Nothing here calls the encoder directly, because a figure produced that way would describe a module rather than the tool a visitor uses.
The convert step runs in every case, including JPEG to JPEG. The compress tool writes the format it is handed and cannot pick one, so the format has to come from the converter; letting the already-JPEG sample skip that step would hand JPEG one fewer generation of loss than WebP and quietly rig the comparison. Both lanes therefore pay the same extra encode, at the converter's own default quality of 80.
Build b1db35c on feat/seo-growth-system, Chromium 151.0.7922.34, Node v20.20.2, Apple M1 Pro — darwin 25.5.0 arm64, served from http://127.0.0.1:3911, which is `BENCH_URL` pointing the run off the default port. Reproduce it with `npm run build` and `npx next start -p 3910` in one terminal and `npm run bench` in another; the runner refuses to start if nothing is serving, and it aborts the run if the build changes underneath it.
Both scores are computed over BT.601 luma with the alpha channel ignored, and both sides are decoded by sharp before comparison so the browser's decoder and libvips cannot disagree their way into the result. PSNR is peak signal-to-noise ratio in decibels. SSIM is structural similarity — Wang et al. 2004, IEEE TIP 13(4), equation 13 — over an 8x8 uniform window, one window per pixel position that fits, averaged over windows. It is not MS-SSIM: there is no multi-scale pyramid and no Gaussian weighting, so a figure from this table does not compare with a paper reporting MS-SSIM.
- The four samples are drawn by a seeded generator, not photographed. They span four content types on purpose; they are not a sample of what visitors actually open.
- One machine, one thermal state, one Chromium build. Bytes and scores are deterministic for a given build; the times in the last column are not, and are not a product claim.
- One encoder per format — MozJPEG and libwebp as this build ships them. Another encoder, or another set of switches, would move every row.
- The run records every request each page made and fails a case if any of them was not a same-origin GET, so the no-upload promise is checked mechanically rather than asserted.
A byte target is a ceiling, so only one sample was a fair fight
Both lanes were asked the same thing: put this picture under 100 KB, then under 50 KB. The tool answers by encoding, reading the real output length back and searching for the highest quality that still fits underneath the number. It is maximising quality subject to a limit, not aiming at a size — which means a file that started smaller than the ceiling comes back larger than it was, at a higher quality than it had.
That is why only one of the four produced a comparison at the same size. The photograph starts at 384.2 KB, so both ceilings squeezed both encoders and both landed just under the number. The screenshot starts at 24 KB, the graphic at 51.66 KB and the illustration at 25.01 KB — every one of them already beneath the 100 KB ceiling, and only the graphic still above the 50 KB one. Where the ceiling stops constraining anything, the two formats stop answering the same question and go their separate ways.
- Photograph, 100 KB ceiling: JPEG came back at 96.97 KB and WebP at 99 KB. Same question, near enough the same answer size, different scores.
- Screenshot: WebP returned 23.98 KB under both ceilings, while JPEG went up to 94.42 KB and 49.84 KB — 393.4% of the source PNG at the wider ceiling, for a lower score than the WebP.
- Illustration: JPEG reached quality 100 and stopped at 48.41 KB, so its 100 KB row and its 50 KB row are identical in every measured column.
The sixteen results
Output size, the quality the panel reported when it reported one, both scores against the original sample, and the wall time from pressing the button to the download appearing. Ratios and input sizes are in the paragraphs around this table rather than in it.
| Sample | To | Ceiling | Out | Quality | PSNR (dB) | SSIM | Time |
|---|---|---|---|---|---|---|---|
| Photograph | JPEG | 100 KB | 96.97 KB | 66 | 36.98 | 0.9568 | 1.31 s |
| Photograph | JPEG | 50 KB | 48.3 KB | 26 | 33.93 | 0.9222 | 1.32 s |
| Photograph | WebP | 100 KB | 99 KB | — | 41.22 | 0.9797 | 804 ms |
| Photograph | WebP | 50 KB | 49.23 KB | — | 36.92 | 0.9566 | 812 ms |
| Screenshot | JPEG | 100 KB | 94.42 KB | 98 | 47.89 | 0.9974 | 1.31 s |
| Screenshot | JPEG | 50 KB | 49.84 KB | 89 | 46.65 | 0.9956 | 1.31 s |
| Screenshot | WebP | 100 KB | 23.98 KB | — | 49.84 | 0.9980 | 810 ms |
| Screenshot | WebP | 50 KB | 23.98 KB | — | 49.84 | 0.9980 | 808 ms |
| Graphic | JPEG | 100 KB | 94.15 KB | 97 | 42.66 | 0.9866 | 806 ms |
| Graphic | JPEG | 50 KB | 47.52 KB | 89 | 42.06 | 0.9802 | 802 ms |
| Graphic | WebP | 100 KB | 62 KB | — | 51.56 | 0.9989 | 308 ms |
| Graphic | WebP | 50 KB | 48.95 KB | 95 | 51.40 | 0.9989 | 803 ms |
| Illustration | JPEG | 100 KB | 48.41 KB | 100 | 47.44 | 0.9965 | 819 ms |
| Illustration | JPEG | 50 KB | 48.41 KB | 100 | 47.44 | 0.9965 | 807 ms |
| Illustration | WebP | 100 KB | 16.48 KB | — | 51.08 | 0.9985 | 303 ms |
| Illustration | WebP | 50 KB | 16.48 KB | — | 51.08 | 0.9985 | 305 ms |
The dashes in the quality column are not missing data. WebP is asked for the ceiling through libwebp's own rate controller, and when that lands underneath it the quality number decided nothing, so the panel refuses to print one. There is one exception in the table: the graphic at the 50 KB ceiling — the only case outside the photograph where a ceiling was still squeezing WebP at all. There the controller overshot, the same bounded quality search JPEG uses took over, and it settled on 95 for 48.95 KB.
What the photograph rows show, and what they do not
Two files of nearly the same size, one of them measurably closer to the original. At the 100 KB ceiling the WebP is 99 KB scoring 41.22 dB and 0.9797; the JPEG is 96.97 KB scoring 36.98 and 0.9568. At 50 KB the gap narrows but holds: 36.92 against 33.93, and 0.9566 against 0.9222.
The interesting row is the diagonal. The 50 KB WebP scores 36.92 dB and 0.9566; the 100 KB JPEG scores 36.98 and 0.9568. That is 0.06 dB and 0.0002 SSIM between them, reached with 49.23 KB instead of 96.97 KB. On this picture, at this size, that is what the newer format buys: roughly half the file for a measured distance from the original the scores cannot separate.
What it does not show is a general rule. This is one drawn scene with a lot of fine grain in it, two ceilings, and one encoder on each side. A picture with smoother gradients and no grain would move both columns, and probably not by the same amount. The only figure that describes your own photograph is the one the panel prints when you run it, so push your own file to a target rather than trusting a row here.
Screenshots and flat art invert the whole question
JPEG was designed for grain and gradient, and a hard boundary between two flat colours is the case it handles worst: it pays for that edge in bytes, and it rings around it when it does not pay enough. A screenshot is almost nothing but hard boundaries and flat areas, so the search has to climb to a very high quality before the ringing stops showing — and at that quality the file is several times the size of the PNG it came from.
The numbers make that concrete. The screenshot's JPEG needed quality 98 and 94.42 KB to score 47.89 dB; its WebP scored 49.84 at 23.98 KB, which is 99.9% of the source and about a quarter of the JPEG. The illustration goes further: its WebP came back at 16.48 KB, 65.9% of the 25.01 KB PNG it started as, and did not move when the ceiling was halved. Neither WebP was anywhere near its ceiling, so neither ceiling was doing any work.
The graphic is the one sample that arrived with an alpha channel, and it needs a caveat rather than a headline. JPEG has no alpha, so the convert step flattened the picture onto black before the compress step ever saw it, and the scoring flattens both sides onto that same black. That is deliberate: the colour underneath a fully transparent pixel is undefined, every encoder writes something different there, and scoring this sample without compositing produced a WebP figure far below the JPEG one — the metric reading the half of the image nobody ever sees, so that number is not in the table. The graphic's 51.56 dB describes what is displayed, and these four rows say nothing about how either format stores transparency. That question belongs to PNG to WebP, where the alpha channel actually survives.
Which one to reach for
For a photograph going onto a web page, the table is one-sided: the same ceiling bought 4.25 dB more at 100 KB, and half the bytes came within 0.06 dB of it. For a file you are handing to a person, the argument runs the other way and has nothing to do with quality — a JPEG opens in everything, and WebP still meets mail clients, older desktop editors and upload forms that will not take it.
- Publishing a photograph: WebP, and let the byte ceiling be the thing you argue about.
- Sending a file to someone, or feeding a form: JPEG, because compatibility is the constraint that actually bites and no score fixes it.
- Flat art or a screenshot: the comparison worth running is not JPEG against WebP but either of them against the PNG you already have, which may well be the smallest and is certainly the sharpest.
- Anything that has to stay pixel-exact: not WebP here. The encoder in this build takes a quality number and has no lossless mode, so a mark with crisp edges stays a PNG.
The three routes that do these jobs are JPG to WebP for photographs, PNG to WebP for a graphic that has to keep its transparency, and WebP to JPG when something at the other end refuses the newer format.
How to read a PSNR or an SSIM figure without over-reading it
PSNR is a single global error term on a logarithmic scale. It says how far the pixels moved and nothing whatsoever about whether a person would notice: three decibels is half the mean squared error, not three percent of anything, and an encoder can lose a few decibels in a place nobody looks or gain them in a place everybody does.
SSIM is closer to how a person compares two pictures, because it looks at local structure rather than at one summed error. The window here is 8x8 and uniform, which is not the Gaussian window the reference implementation uses and not the multi-scale version most papers report, so treat a figure from this table as comparable with the other figures in this table and with nothing else. Rows above 0.99 are all in territory where the two pictures are hard to tell apart at viewing size; the photograph's 0.9222 at 50 KB is not.
And the rule that keeps a byte figure honest: a size quoted without a score is half a sentence, because any encoder can reach 50 KB by throwing the picture away. The pairing is what carries meaning, which is why the table above never prints one column without the other.
Where to go next
- JPG to WebP — Run the conversion this guide measured on a photograph of your own, and read the real before and after bytes for your file rather than for a drawn sample.
- PNG to WebP — Convert a graphic to WebP with its alpha channel intact, which is the one thing these flattened rows could not measure.
- WebP to JPG — Go back the other way when the place the picture is going will not open a WebP.
- Compress Image — Type a byte ceiling for your own picture and see the quality the search settles on, measured on that file instead of on this table.
Frequently asked questions
Is a WebP always smaller than a JPEG?
At a fixed byte ceiling neither one is smaller, because both are asked to fit under the same number — what differs is the score they reach at that size. The two go their own ways only when the source is already under the ceiling and nothing is squeezing them: at the 100 KB ceiling the graphic came back as 62 KB of WebP against 94.15 KB of JPEG, and the illustration as 16.48 KB against 48.41 KB.
Why is the quality column empty for most of the WebP rows?
Because on those rows the quality number decided nothing. WebP is asked for the byte target through libwebp's own rate controller, and where that lands under the ceiling the panel prints no quality rather than printing a number the encode ignored. When the controller overshoots, the same bounded quality search JPEG uses takes over and the quality is reported — which is what happened on the graphic at the 50 KB ceiling.
Why did asking for 100 KB make my file bigger?
The target is a ceiling, not a goal. The search returns the best quality that still fits underneath it, so a file that was already smaller than the number comes back larger and better than it went in. That is the whole story of 6 rows here: the screenshot's JPEG went from 24 KB to 94.42 KB at quality 98, because that is the best it could do without breaching 100 KB.
Can I get a lossless WebP from these tools?
No. The WebP encoder loaded into these pages takes a quality number and nothing else, so every WebP written here is lossy. For a mark or a screenshot that has to stay pixel-exact, keep the PNG.
Do these numbers describe my own images?
Only loosely. Four drawn samples on one laptop span four kinds of picture; they are not a sample of what people actually open, and a photograph with less grain or a screenshot at a different scale would move every row. The figure that describes your file is the one the compress panel prints for it, which is measured on your own device with nothing uploaded.