Open a WebP file in a hex editor. The first twelve bytes:
52 49 46 46 ?? ?? ?? ?? 57 45 42 50
That is RIFF/WAVE-style container header. 52 49 46 46 spells "RIFF" in ASCII. The next four bytes are the file size in little-endian uint32. 57 45 42 50 spells "WEBP". A WebP file is not a raw bitstream. It is a container, just like WAV audio or AVI video, built from the same RIFF specification Microsoft introduced in 1991. Inside that container sits a VP8 video frame, repurposed for a still image. That choice — 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 looked like a clear case for switching. 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:
- Predictive coding: 14 different spatial prediction modes per pixel.
- Color cache: A hash table of recently seen colors to exploit local repetition.
- LZ77 back-references: Like PNG's DEFLATE, but with a 2D spatial-aware matching.
- Huffman + arithmetic hybrid: Entropy coding 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:
- Alpha channel: 8-bit alpha encoded separately, compressed with lossless WebP's entropy coder.
- Animation: Multiple frames with timing metadata, basically a stripped VP8 video.
- EXIF metadata: Camera and geolocation data.
- XMP metadata: Adobe-style processing instructions.
- ICC color profile: 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:
Smaller files: 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: One format replaces both JPEG and PNG for most use cases. Lossy mode for photos, lossless mode for graphics, alpha channel for transparency, animation for short sequences. Developers only need to know one format instead of three.
Browser-native decode: 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: WebP supports incremental display as data arrives, similar to JPEG's progressive mode. For slow connections, a recognizable image appears after receiving ~30% of the file.
Animation: 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: In 2010, WebP encoding was roughly 8x slower than libjpeg. That gap has narrowed — libwebp in 2026 is about 2-3x slower than libjpeg-turbo — but it still matters for batch workflows. A photographer exporting 1,000 images will wait noticeably longer.
No 16-bit or HDR support: WebP is strictly 8-bit per channel. For wide-gamut photography, medical imaging, or HDR content, WebP is unusable. HEIC, AVIF, and JPEG XL all support higher bit depths.
No lossless JPEG recompression: JPEG XL can take an existing JPEG and recompress it losslessly for ~20% savings. WebP cannot. Converting a JPEG to WebP requires full re-encoding, which introduces generation loss.
Tooling gaps: 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: 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 has no transparency, no animation, no lossless mode, and visible artifacts at quality 75. WebP beats it on nearly every metric. Yet the 2025 Web Almanac puts JPEG at roughly 46% of all web images versus WebP at 19%.
This is not a technical failure. It is about network effects and switching costs.
JPEG is the QWERTY of image formats. Every camera saves JPEG by default. Every phone displays JPEG natively. Every printer accepts JPEG. Every social network, CMS, CDN, and email client handles JPEG 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 sits somewhere between success and failure — a partial success that 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 real legacy is showing that the web can adopt new image formats when a major browser vendor commits to one. It paved the way for AVIF. It forced Apple to support non-JPEG/PNG formats natively. It showed that transparency and animation belong in a single container.
But it also proved that technical superiority is not enough. Ubiquity, inertia, and ecosystem alignment 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, for any site willing to convert its image pipeline. The compression is real, the features are useful, and the 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 so badly that migration is unavoidable. JPEG did not fail badly enough. WebP was not easy enough. And 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.



