The .webp extension hides two unrelated formats, and only one of them can be perfect
JPEG is lossy. PNG is lossless. You know which you have from the extension.
WebP is both, and the extension tells you nothing. A .webp file might hold a format closely related to a video frame, or it might hold a format with no relation to video at all, built on completely different machinery. They share a container, four letters at the end of a filename, and essentially nothing else.
This matters more than a piece of trivia, because the choice between them is not a quality slider. On the wrong content, the lossy one costs more bytes than the lossless one while still throwing information away.
The same picture, saved twice
A WebP file is a RIFF container: the ASCII tag RIFF, a length, the tag WEBP, and then a series of chunks, each with a four-character name and a length. The chunk names are the whole story.
Saving one image both ways:
lossless.webp — 213,092 bytes
chunk "VP8L" 213,071 bytes
lossy80.webp — 15,872 bytes
chunk "VP8 " 15,852 bytes
One chunk each, and the name is different. VP8 — with a trailing space, because the name must be four characters — is a still frame of VP8, the video codec. VP8L is WebP Lossless, which is not VP8 at all and not a video codec: it is a predictive transform plus Huffman coding, far closer in spirit to PNG than to anything in a video file.
Two separate specifications. A program that reads one does not thereby read the other.
Where each one wins
The lossless file above is thirteen times larger than the lossy one, which looks like a settled argument until you ask what happens as quality rises. Lossless has no quality setting — it is perfect, and perfect costs whatever it costs. Lossy has a slider, and the slider has an end.
On an interface screenshot, where the best lossless WebP comes to 41,246 bytes:
lossy quality bytes vs lossless PSNR
80 36,448 88% 35.91 dB
85 42,298 103% 36.30 dB
90 50,274 122% 36.54 dB
95 64,254 156% 36.72 dB
100 77,140 187% 36.77 dB
At quality 85 the lines have already crossed. At quality 95 the lossy file is half as large again as the perfect one. At quality 100 — the setting whose name promises everything — it is 87% larger than lossless and still lossy, sitting at 36.77 dB when lossless is by definition exact.
Anyone who has ever reached for quality 100 expecting a clean copy has paid nearly double for a worse result.
The crossover is a property of the content, not a universal number. Running the same test on three kinds of picture:
app icon, flat colour lossless 210,906 B lossy never overtakes
interface screenshot lossless 41,246 B lossy overtakes at q85
photograph lossless 572,584 B lossy never overtakes
On photographs the lossy mode wins by an enormous margin and keeps winning — its whole design is continuous tone. On flat graphics it also wins on raw size, but as the table above shows, it stops being the cheaper option exactly where people tend to push the slider to avoid artefacts on text.
The ceiling it cannot pass
The interesting number in that table is not the size. It is that the quality stops moving: 36.54 dB at quality 90, 36.77 at quality 100. Twenty-seven thousand extra bytes bought a quarter of a decibel.
Splitting the error into brightness and colour says exactly where the wall is:
quality bytes Y Cb Cr
80 36,448 45.93 38.73 38.88
90 50,274 50.05 39.11 39.27
100 77,140 55.51 39.23 39.41
lossless 41,246 99.00 99.00 99.00
Between quality 80 and 100 the brightness channel improves by 9.6 dB. The two colour channels improve by half a decibel and then stop. The combined score is limited by the worst channel, so the whole file plateaus even though the encoder is clearly still working on the luminance.
The reason is structural rather than a tuning choice. Lossy WebP inherits VP8's picture format, and VP8 works in YUV with the colour planes at half resolution in each direction — three quarters of the colour samples are discarded before encoding begins. No quality setting reverses that, because by the time the quality setting applies, the samples are already gone.
Lossless WebP has no such step. It codes red, green and blue directly, which is why its row reads 99 across the board — the measurement's way of saying the files are identical.
So "lossy WebP at maximum quality" is not a near-lossless mode and cannot be made into one. If the requirement is an exact copy, the only WebP that provides it is the other one.
And against JPEG?
That is the question most people actually have, and it is the one case where the lossy mode's video ancestry pays off completely. Both encoders given the same photograph, both scored by PSNR, then matched on quality rather than on settings:
equal quality JPEG lossy WebP saving
38.0 dB q46 49,162 B q32 18,654 B 62%
40.0 dB q68 61,874 B q52 23,802 B 62%
42.0 dB q80 83,642 B q78 33,178 B 60%
Around 60% smaller, and it holds steady across the range rather than being an artefact of one operating point. VP8 predicts each block from its neighbours before coding anything, where JPEG transforms every block from scratch; on a photograph, where neighbouring blocks are nearly the same, that prediction is almost free information.
One caveat on these particular numbers: the source was itself a JPEG, so the JPEG column is partly re-encoding its own artefacts. That is the common real-world case, and it is the same picture given to both, but a pristine camera original would narrow the gap somewhat.
A transparent lossy WebP is two codecs in one file
VP8 came from video, and video has no alpha channel. WebP still needed transparency, so the container does something unusual: it stores the colour and the transparency with different algorithms, in different chunks.
The same image with an alpha ramp, saved both ways:
lossy q80 16,052 bytes
VP8X 10 B extended header, required once there is alpha
ALPH 124 B the alpha channel, compressed separately and LOSSLESSLY
VP8 15,882 B the colour, lossy
lossless 233,926 bytes
VP8L 233,905 B colour and alpha together, one codec
A lossy WebP with transparency therefore contains a lossy codec and a lossless codec side by side, plus an extended header to announce that the file is not the simple case. The transparency in a "lossy" WebP is, in fact, exact.
That is a sensible design — soft edges would look terrible with lossy alpha — but it is worth knowing when a file is larger than expected, and it explains why a reader that handles plain lossy WebP can still fail on a transparent one. It is a different arrangement of chunks and needs code for all three.
Why lossless WebP beats PNG, and one explanation that turned out to be wrong
Against the smallest PNGs we could produce — the unfiltered ones from the previous article, which were already 23% below what a normal encoder writes — lossless WebP still wins comfortably:
screenshot-home best PNG 60,647 lossless WebP 41,246 32% smaller
screenshot-tool best PNG 61,907 lossless WebP 39,272 37% smaller
screenshot-why best PNG 99,005 lossless WebP 58,162 41% smaller
The obvious candidate explanation is VP8L's subtract-green transform. Before coding anything it replaces red with red-minus-green and blue with blue-minus-green, on the theory that the three channels of a natural image move together, so the differences are smaller and more compressible than the originals.
It is a tidy story. It also does not survive being measured. Deflating the raw planes both ways, as-is against subtract-green:
UI screenshot 59,596 -> 61,341 2% WORSE
app icon 457,183 -> 470,458 2% WORSE
photograph 1,091,838 -> 1,070,341 2% better
Two percent either way is nothing, and on two of the three images the transform made things worse. Whatever lossless WebP is doing to beat PNG by a third, decorrelating the colour channels is not the main part of it.
This is not quite a fair test of VP8L — deflate is not its entropy coder, and the transform is designed to feed something else. But it is a fair test of the explanation, which was that the channels are correlated enough for the subtraction alone to pay. On this content they are not.
The likelier answer is the one that connects to the previous article. PNG picks one filter per scanline from five. VP8L picks a predictor per block from fourteen, and carries its own entropy coder rather than handing the result to DEFLATE. Per-row choice was exactly what was shown to cost PNG real bytes on this kind of image; per-block choice is the fix, and it is not available inside PNG.
So which one
The measurements give a rule that does not need judgement:
Photographs: lossy. It is what VP8 was built for, it wins by five to ten times, and the losses land where the eye is least sensitive.
Screenshots, diagrams, logos, anything with text: lossless. Not only because artefacts around text are obvious, but because the lossy mode stops being cheaper somewhere around quality 85 on exactly this content. You can pay more and still get less.
Anything that must survive another edit: lossless. The ceiling above is not a setting; it is the format.
Transparency: check the size either way. The separate alpha chunk is cheap when the alpha is simple and not cheap when it is not.
When this does not apply
One encoder, one set of defaults. These files came from libwebp through a standard imaging library at its usual settings. The crossover point in particular will move with encoder effort settings — a slower lossless pass finds more, which pushes the crossover down, not up.
The crossover is content-specific. Quality 85 is where it landed for one screenshot. A busier screenshot crosses later, a flatter one earlier. The method is the portable part, not the number.
PSNR is a proxy. It is reproducible and it is what makes the ceiling visible as a number, but it does not model how text artefacts look, which is the thing that actually matters on a screenshot. In that respect PSNR understates the case for lossless here.
Lossless WebP is not always smaller than PNG. It was by 29 to 38% on the files measured here, but a PNG with a small palette can be very hard to beat.
Animation is a third thing again. Animated WebP uses an ANMF chunk structure and can mix lossy and lossless frames in one file, which is outside everything above.
Checking it yourself
You do not need a tool. Open any .webp in a hex viewer and read the first sixteen bytes:
lossless: 52 49 46 46 50 d5 08 00 57 45 42 50 56 50 38 4c
lossy: 52 49 46 46 f8 89 00 00 57 45 42 50 56 50 38 20
R I F F (length) W E B P V P 8 ?
If the sixteenth byte is 4C — the letter L — the file is lossless. If it is 20, an ASCII space, it is lossy. If instead the four bytes read VP8X, the file is the extended kind and the real codec chunk comes a little later, after the alpha or metadata chunks.
Three bytes into a file, available in any editor, and it settles a question the filename cannot answer.
Sources
- RFC 6386 — VP8 Data Format and Decoding Guide
- WebP Lossless Bitstream Specification
- WebP Container Specification (RIFF)
Tools mentioned
← All articles · corrections: editorial policy