Open a WebP file in a hex editor and the first twelve bytes are:
52 49 46 46 ?? ?? ?? ?? 57 45 42 50
That is a RIFF/WAVE-style container header. 52 49 46 46 spells "RIFF" in ASCII. The next four bytes store the file size as a little-endian uint32. 57 45 42 50 spells "WEBP". A WebP file is not a raw bitstream. It is a container, the same kind used by WAV audio and AVI video, built from the RIFF specification Microsoft introduced in 1991. Inside sits a VP8 video frame, repurposed for a still image. Using a video codec for still images shapes everything about how WebP behaves in practice.
Google announced WebP on September 30, 2010. The pitch was simple: same visual quality as JPEG, but 25-34% smaller. For PNG, the claim was even bolder: 26% smaller for lossless images. Images account for roughly half of all bytes transferred on the web. Those numbers suggested an obvious migration. They were not.
The acquisition that created WebP
WebP did not come from an image lab. It came from a video codec.
In February 2010, Google acquired On2 Technologies for approximately $124 million. On2 was a video compression company with a long history; their codecs powered Flash video, Skype video calls, and AOL streaming. Their flagship product was VP8, a video codec designed to compete with H.264 without patent royalties.
Google open-sourced VP8 in May 2010 under the name WebM, bundling it with the Vorbis audio codec and the Matroska container. The goal was clear: build a patent-free video stack to challenge MPEG-LA's H.264 licensing pool, which was starting to demand royalties from web video streaming.
But Google had a second use case in mind. VP8's intra-frame compression (the compression of a single video frame without reference to other frames) functioned as a still-image codec. The prediction modes, transform coding, and entropy coding that made VP8 efficient for video worked just as well for single frames. Google extracted the intra-frame mode, wrapped it in a RIFF container, and called it WebP.
The name followed simple logic. "Web" for its intended platform. "P" because image formats tend to end that way: JPEG, PNG, BMP, TIFF. Technically, WebP was a video frame packaged as a photo.
Why build a new format at all?
By 2010, JPEG was eighteen years old and PNG was fourteen. Both were entrenched. Why bother?
JPEG's limitations were well understood:
- No transparency. A JPEG pixel is either fully opaque or you need a separate mask.
- No animation. Animated JPEG does not exist as a standard.
- Lossy-only. JPEG's baseline spec has no lossless mode. (JPEG-LS and JPEG 2000 exist, but neither is web-compatible.)
- 8-bit color depth per channel. No wide-gamut or HDR support in the baseline.
- Blocking artifacts at low quality. The 8 x 8 DCT grid is visible at quality settings below 75.
PNG had similar constraints:
- No lossy mode. PNG is always lossless. A 12 MP photograph as PNG is 15-25 MB.
- Large files for photographs. PNG's DEFLATE compression cannot compete with DCT-based psycho-visual discard.
- No animation in the base spec. APNG exists but took years to gain browser support.
The pitch was a single format that handled lossy and lossless compression, transparency, and animation, all in smaller files than the incumbents.
How WebP actually works
WebP has two fundamentally different internal formats: lossy WebP (VP8 intra-frame) and lossless WebP (a separate codec, also derived from VP8 research).
Lossy WebP: VP8 intra
Lossy WebP stores a VP8 bitstream inside a RIFF container. The encoding pipeline resembles JPEG's, with a few important differences:
| Stage | JPEG | Lossy WebP |
|---|---|---|
| Transform | 8 x 8 DCT | 4 x 4 or 16 x 16 integer DCT-like transform |
| Prediction | None (intra only) | 4 intra-prediction modes per 4 x 4 block |
| Chroma subsampling | 4:2:0 default | 4:2:0 default |
| Entropy coding | Huffman | Binary arithmetic coding |
| Bit depth | 8-bit | 8-bit |
The intra prediction is where most of the gain comes from. JPEG encodes each 8 x 8 block independently. WebP predicts each 4 x 4 block from its already-encoded neighbors (top, left, or both), then encodes only the prediction error. For smooth gradients and large flat regions, the error is tiny, and the compression ratio improves significantly.
The arithmetic coder is also more efficient than JPEG's Huffman coding, typically 5-10% additional savings for the same quality.
Google's own benchmarks from 2010 claimed:
| Metric | WebP vs JPEG |
|---|---|
| Average file size reduction | 25-34% at equivalent SSIM |
| Encode speed | ~8x slower than libjpeg |
| Decode speed | Comparable to libjpeg |
Encode speed was the hidden cost. Producing a WebP file took significantly more CPU than JPEG. For photographers exporting hundreds of images, that mattered.
Lossless WebP
Lossless WebP uses a completely different codec. It is not VP8. It is a custom format combining four techniques.
Predictive coding provides 14 different spatial prediction modes per pixel. A color cache keeps a hash table of recently seen colors to exploit local repetition. LZ77 back-references work like PNG's DEFLATE, but with 2D spatial-aware matching. The entropy coder is a Huffman and arithmetic hybrid that adapts to local statistics.
Google claimed 26% smaller files than PNG on average. In practice, savings vary widely. Simple graphics with large flat regions see little benefit, while photographs with fine texture can see 30-40% reduction.
Extended WebP (VP8X)
The VP8X chunk extends WebP with additional features.
It adds an alpha channel, animation, EXIF and XMP metadata, and ICC color profiles. Alpha is 8-bit and encoded separately, compressed with lossless WebP's entropy coder. Animation stores multiple frames with timing metadata, basically a stripped VP8 video. EXIF holds camera and geolocation data. XMP holds Adobe-style processing instructions. The ICC profile enables wide-gamut and HDR color management.
A VP8X file begins with a VP8X chunk header, followed by flags that indicate which extensions are present.
The file format
WebP is a RIFF container. The byte layout is easy to follow if you understand RIFF.
RIFF container structure
Bytes 0-3: "RIFF" (0x52 0x49 0x46 0x46)
Bytes 4-7: File size - 8 (little-endian uint32)
Bytes 8-11: "WEBP" (0x57 0x45 0x42 0x50)
Bytes 12-15: Chunk FourCC — "VP8 ", "VP8L", or "VP8X"
Bytes 16-19: Chunk size (little-endian uint32)
Bytes 20+: Chunk data
VP8X extended header
If the FourCC at bytes 12-15 is "VP8X" (0x56 0x50 0x38 0x58):
Bytes 20-23: Chunk size = 10 (little-endian uint32)
Bytes 24: Flags byte
Bit 0: Animation present
Bit 1: XMP metadata present
Bit 2: EXIF metadata present
Bit 3: Alpha channel present
Bit 4: ICC profile present
Bits 5-7: Reserved
Bytes 25-27: Canvas width - 1 (little-endian uint24)
Bytes 28-30: Canvas height - 1 (little-endian uint24)
The canvas dimensions are stored as width - 1 and height - 1, so a 1200 x 675 image stores 1199 and 674. The maximum canvas size is 16,777,215 x 16,777,215 pixels.
Chunk types
| FourCC | Content | Compression |
|---|---|---|
VP8 | VP8 bitstream (lossy) | VP8 intra |
VP8L | VP8L bitstream (lossless) | Custom lossless |
VP8X | Extended header + flags | None |
ALPH | Alpha channel data | Lossless WebP entropy |
ANMF | Animation frame | VP8/VP8L per frame |
ICCP | ICC color profile | None |
EXIF | EXIF metadata | None |
XMP | XMP metadata | None |
Detecting WebP by reading the file signature
Do not trust the .webp extension. Read the first 16 bytes and parse the RIFF header.
Exact byte layout of a simple lossy WebP:
Bytes 0-3: "RIFF"
Bytes 4-7: File size (little-endian uint32)
Bytes 8-11: "WEBP"
Bytes 12-15: "VP8 " (lossy) or "VP8L" (lossless) or "VP8X" (extended)
TypeScript in the browser:
interface WebPInfo {
valid: boolean
type: "lossy" | "lossless" | "extended" | "unknown"
width?: number
height?: number
hasAlpha?: boolean
isAnimated?: boolean
}
async function inspectWebP(file: File): Promise<WebPInfo> {
const buffer = await file.slice(0, 30).arrayBuffer()
const bytes = new Uint8Array(buffer)
if (bytes.length < 12) return { valid: false, type: "unknown" }
const riff = String.fromCharCode(...bytes.slice(0, 4))
const webp = String.fromCharCode(...bytes.slice(8, 12))
if (riff !== "RIFF" || webp !== "WEBP") {
return { valid: false, type: "unknown" }
}
const type = String.fromCharCode(...bytes.slice(12, 16))
if (type === "VP8 ") {
// Lossy: width/height at bytes 26-29
const w = bytes[26] | (bytes[27] << 8)
const h = bytes[28] | (bytes[29] << 8)
return { valid: true, type: "lossy", width: w, height: h, hasAlpha: false }
}
if (type === "VP8L") {
// Lossless: dimensions packed in bits of bytes 21-24
const bits =
bytes[21] | (bytes[22] << 8) | (bytes[23] << 16) | (bytes[24] << 24)
const w = (bits & 0x3fff) + 1
const h = ((bits >> 14) & 0x3fff) + 1
const alpha = ((bits >> 28) & 0x01) !== 0
return {
valid: true,
type: "lossless",
width: w,
height: h,
hasAlpha: alpha,
}
}
if (type === "VP8X") {
const flags = bytes[20]
const w = (bytes[24] | (bytes[25] << 8) | (bytes[26] << 16)) + 1
const h = (bytes[27] | (bytes[28] << 8) | (bytes[29] << 16)) + 1
return {
valid: true,
type: "extended",
width: w,
height: h,
hasAlpha: (flags & 0x10) !== 0,
isAnimated: (flags & 0x02) !== 0,
}
}
return { valid: false, type: "unknown" }
}
Python
import struct
from typing import TypedDict
class WebPInfo(TypedDict):
valid: bool
type: str
width: int | None
height: int | None
has_alpha: bool | None
is_animated: bool | None
def inspect_webp(path: str) -> WebPInfo:
with open(path, "rb") as f:
header = f.read(30)
if len(header) < 12:
return {"valid": False, "type": "unknown"}
if header[:4] != b"RIFF" or header[8:12] != b"WEBP":
return {"valid": False, "type": "unknown"}
chunk_type = header[12:16]
if chunk_type == b"VP8 ":
w, h = struct.unpack("<HH", header[26:30])
return {"valid": True, "type": "lossy", "width": w, "height": h,
"has_alpha": False, "is_animated": None}
if chunk_type == b"VP8L":
bits = struct.unpack("<I", header[21:25])[0]
w = (bits & 0x3FFF) + 1
h = ((bits >> 14) & 0x3FFF) + 1
alpha = ((bits >> 28) & 0x01) != 0
return {"valid": True, "type": "lossless", "width": w, "height": h,
"has_alpha": alpha, "is_animated": None}
if chunk_type == b"VP8X":
flags = header[20]
w = struct.unpack("<I", header[24:27] + b"\x00")[0] + 1
h = struct.unpack("<I", header[27:30] + b"\x00")[0] + 1
return {"valid": True, "type": "extended", "width": w, "height": h,
"has_alpha": bool(flags & 0x10), "is_animated": bool(flags & 0x02)}
return {"valid": False, "type": "unknown"}
Higher-level option with Pillow:
from PIL import Image
with Image.open("image.webp") as img:
print(img.format) # WEBP
print(img.mode) # RGB or RGBA
print(img.size) # (width, height)
print(img.is_animated) # True if animated
Or with the webp library:
import webp
info = webp.WebPInfo("image.webp")
print(info.width, info.height, info.has_alpha)
Go
package main
import (
"encoding/binary"
"fmt"
"os"
)
type WebPInfo struct {
Valid bool
Type string
Width int
Height int
HasAlpha bool
IsAnimated bool
}
func inspectWebP(path string) (WebPInfo, error) {
f, err := os.Open(path)
if err != nil {
return WebPInfo{}, err
}
defer f.Close()
buf := make([]byte, 30)
if _, err := f.Read(buf); err != nil {
return WebPInfo{}, err
}
if string(buf[:4]) != "RIFF" || string(buf[8:12]) != "WEBP" {
return WebPInfo{Valid: false, Type: "unknown"}, nil
}
typeStr := string(buf[12:16])
switch typeStr {
case "VP8 ":
w := int(binary.LittleEndian.Uint16(buf[26:28]))
h := int(binary.LittleEndian.Uint16(buf[28:30]))
return WebPInfo{Valid: true, Type: "lossy", Width: w, Height: h, HasAlpha: false}, nil
case "VP8L":
bits := binary.LittleEndian.Uint32(buf[21:25])
w := int(bits&0x3FFF) + 1
h := int((bits>>14)&0x3FFF) + 1
alpha := ((bits >> 28) & 0x01) != 0
return WebPInfo{Valid: true, Type: "lossless", Width: w, Height: h, HasAlpha: alpha}, nil
case "VP8X":
flags := buf[20]
w := int(binary.LittleEndian.Uint32(append(buf[24:27], 0))) + 1
h := int(binary.LittleEndian.Uint32(append(buf[27:30], 0))) + 1
return WebPInfo{
Valid: true, Type: "extended", Width: w, Height: h,
HasAlpha: (flags & 0x10) != 0, IsAnimated: (flags & 0x02) != 0,
}, nil
}
return WebPInfo{Valid: false, Type: "unknown"}, nil
}
PHP
function inspectWebP(string $path): array {
$header = file_get_contents($path, false, null, 0, 30);
if (strlen($header) < 12) {
return ["valid" => false, "type" => "unknown"];
}
if (substr($header, 0, 4) !== "RIFF" || substr($header, 8, 4) !== "WEBP") {
return ["valid" => false, "type" => "unknown"];
}
$type = substr($header, 12, 4);
if ($type === "VP8 ") {
$w = unpack("v", substr($header, 26, 2))[1];
$h = unpack("v", substr($header, 28, 2))[1];
return ["valid" => true, "type" => "lossy", "width" => $w, "height" => $h, "has_alpha" => false];
}
if ($type === "VP8L") {
$bits = unpack("V", substr($header, 21, 4))[1];
$w = ($bits & 0x3FFF) + 1;
$h = (($bits >> 14) & 0x3FFF) + 1;
$alpha = (($bits >> 28) & 0x01) !== 0;
return ["valid" => true, "type" => "lossless", "width" => $w, "height" => $h, "has_alpha" => $alpha];
}
if ($type === "VP8X") {
$flags = ord($header[20]);
$w = unpack("V", substr($header, 24, 3) . "\x00")[1] + 1;
$h = unpack("V", substr($header, 27, 3) . "\x00")[1] + 1;
return [
"valid" => true, "type" => "extended",
"width" => $w, "height" => $h,
"has_alpha" => (bool)($flags & 0x10),
"is_animated" => (bool)($flags & 0x02),
];
}
return ["valid" => false, "type" => "unknown"];
}
With fileinfo:
$finfo = new finfo(FILEINFO_MIME_TYPE);
$mime = $finfo->file('image.webp');
// image/webp
ImageMagick CLI
magick identify -verbose image.webp | grep "Format:"
# Format: WEBP (WebP Image Format)
Full metadata extraction:
magick identify -verbose image.webp
This outputs width, height, color depth, alpha presence, compression type, and ICC profile information.
Or simply:
file image.webp
# image.webp: RIFF (little-endian) data, Web/P image
Where WebP delivers
WebP performs well in specific areas.
File size is the headline. On Google's reference corpus, lossy WebP averaged 25-34% smaller than JPEG at the same SSIM. Lossless WebP averaged 26% smaller than PNG. For high-traffic sites, those savings translate directly into bandwidth costs and faster page loads.
Feature consolidation also helps. One format replaces both JPEG and PNG for most use cases. Lossy mode handles photos, lossless mode handles graphics, the alpha channel handles transparency, and animation handles short sequences. Developers only need to know one format instead of three.
Browser-native decode is another advantage. Chrome, Firefox, Safari, and Edge all ship hardware-accelerated or highly optimized software WebP decoders. Decode speed is comparable to JPEG on desktop and within 10-20% on mobile.
Progressive decoding lets WebP display incrementally as data arrives, similar to JPEG's progressive mode. For slow connections, a recognizable image appears after receiving roughly 30% of the file.
Animation rounds out the list. Animated WebP files are typically 60-80% smaller than animated GIFs at equivalent visual quality, with full 24-bit color and 8-bit alpha per frame.
Where WebP falls short
WebP's problems are less about the specification and more about the ecosystem around it.
Encode speed was a known weakness. In 2010, WebP encoding was roughly 8x slower than libjpeg. The gap has narrowed; libwebp in 2026 is about 2-3x slower than libjpeg-turbo. It still matters for batch workflows. A photographer exporting 1,000 images will wait noticeably longer.
WebP is strictly 8-bit per channel, so it has no 16-bit or HDR support. For wide-gamut photography, medical imaging, or HDR content, WebP is unusable. HEIC, AVIF, and JPEG XL all support higher bit depths.
WebP cannot do lossless JPEG recompression. JPEG XL can take an existing JPEG and recompress it losslessly for about 20% savings. Converting a JPEG to WebP requires full re-encoding, which introduces generation loss.
Tooling gaps slowed adoption. Photoshop did not support WebP natively until 2022. ImageMagick's WebP support required a libwebp plugin compilation, which many distributions omitted by default. Many content management systems still generate JPEG/PNG by default.
The VP8 patent cloud also made some organizations cautious. Google released VP8 with a patent indemnification promise, but the codec's patent landscape was never as clean as PNG's or JPEG's. Some organizations avoided WebP because they did not trust Google's legal shield to hold up in court.
Why the "inferior" format won
JPEG is thirty-four years old. It lacks transparency, animation, and a lossless mode, and it shows visible artifacts at quality 75. WebP beats it on most metrics. Yet the 2025 Web Almanac puts JPEG at roughly 46% of all web images versus WebP at 19%.
That gap is not a technical failure. It reflects network effects and switching costs.
JPEG is the QWERTY of image formats. Cameras save it by default, phones display it natively, printers accept it, and every social network, CMS, CDN, and email client handles it without plugins, codecs, or conversion. The format is so universal that "image" and "JPEG" are functionally synonymous for most users.
WebP's adoption curve tells the story:
| Year | Milestone |
|---|---|
| 2010 | Google announces WebP (Chrome 8) |
| 2012 | Chrome 23 adds lossless and alpha support |
| 2013 | Chrome adds animated WebP |
| 2014 | Android 4.0+ adds native WebP support |
| 2015 | Facebook converts all mobile photos to WebP |
| 2016 | Safari 14 adds WebP support |
| 2020 | Universal browser support achieved |
| 2022 | Photoshop adds native WebP export |
| 2025 | WebP at 19% of web images per Web Almanac |
Chrome adopted WebP early because Google controlled both the browser and the format. Facebook adopted it because it saved petabytes of bandwidth. But the long tail of the web (WordPress blogs, small e-commerce sites, enterprise CMS deployments, email newsletters) moved slowly or not at all.
The larger problem was Apple's ecosystem. iPhones saved HEIC by default, not WebP. macOS Preview did not support WebP until macOS 11 Big Sur (2020). The iOS share sheet did not offer WebP export. For photographers, designers, and social media creators working primarily on Apple devices, WebP was invisible.
Meanwhile, AVIF arrived in 2019 with better compression than WebP and royalty-free licensing from the Alliance for Open Media. Chrome, Firefox, and Safari all ship AVIF. Cloudflare and Cloudinary serve AVIF automatically. WebP became a stepping stone, better than JPEG but already being leapfrogged by the next generation.
Where WebP stands today
WebP landed somewhere in the middle. It met some of its goals and missed others.
For developers starting new projects in 2026, WebP is the pragmatic default when images need transparency or animation. It is smaller than PNG for lossless graphics and smaller than JPEG for photographs. Browser support is universal. Encode tooling is mature.
But WebP did not replace JPEG. It carved out a niche alongside it, the same niche PNG already held, just with smaller files. The vision of "one format for all images" did not materialize.
What this looks like in practice:
| Use case | Best format | Why |
|---|---|---|
| Photographs (legacy) | JPEG | Universal, fast encode, small enough |
| Photographs (new) | AVIF | 30% smaller than WebP, royalty-free |
| Photographs (fallback) | WebP | 25% smaller than JPEG, universal support |
| Lossless graphics | WebP or PNG | WebP is smaller; PNG is the safe fallback |
| Transparency | WebP or PNG | WebP has smaller files; PNG is the safe fallback |
| Animation | WebP or AVIF | Both beat GIF by 60-80%; AVIF is newer |
| Wide-gamut / HDR | AVIF or JPEG XL | 10+ bit depth, ICC/ICC v4 support |
| Print workflows | TIFF or JPEG XL | CMYK, 16-bit, lossless JPEG recompression |
WebP's most lasting effect may be proving that the web can adopt a new image format when a major browser vendor pushes it. That made AVIF easier to sell. It forced Apple to support non-JPEG/PNG formats natively. It also established that transparency and animation can live in a single container.
But it also proved that better compression alone does not win. Ubiquity and inertia matter more than compression ratios. JPEG will outlive WebP not because it is better, but because it is already everywhere.
What actually happened
WebP was built from a video codec to solve a bandwidth problem. It did solve that problem for Google, for Facebook, and for any site willing to convert its image pipeline. The compression is real and the features are useful. Browser support is complete.
But the web does not switch formats because a white paper says the new one is 25% smaller. It switches when the new format is easier to use than the old one, or when the old one fails badly enough to force migration. JPEG did not fail that badly. WebP was not easy enough. By the time WebP became easy, AVIF had already arrived with a bigger number on the spec sheet.
WebP is the Betamax of image formats: technically solid, well-supported, and then overtaken by something that arrived slightly later with better marketing and broader backing. It will not disappear. It will coexist with JPEG, PNG, AVIF, and whatever comes next, serving the same role PNG serves today: the safe, capable fallback that works everywhere.
If you have PNG files that need smaller footprints for the web, PNG to WebP converts them locally in your browser — no uploads, no server processing. For JPEGs that need transparency or animation, JPG to WebP handles the conversion with quality control. And when you need the universal fallback, WebP to PNG and WebP to JPG bring WebP files back to formats that open in every viewer.



