用十六進位編輯器開啟 WebP 檔案。前十二個位元組:
52 49 46 46 ?? ?? ?? ?? 57 45 42 50
這是 RIFF/WAVE 風格的容器標頭。52 49 46 46 是 ASCII 寫成的 "RIFF"。接下來四個位元組是小端序無符號32位元整數的檔案大小。57 45 42 50 是 ASCII 寫成的 "WEBP"。WebP 檔案不是原始位元流,它是一個容器,就像 WAV 音訊或 AVI 影片,採用 Microsoft 在 1991 年推出的同一套 RIFF 規格。在這個容器裡面放的是一個 VP8 影片影格,被重新用於靜態圖像。這個設計選擇——把視訊編解碼器拿來處理照片——是理解 WebP 最重要的一件事。
Google 在 2010 年 9 月 30 日宣布 WebP。賣點很簡單:視覺品質與 JPEG 相同,但檔案小 25-34%。對 PNG 的說法更大膽——無損圖像小 26%。在網路上圖像位元組約佔總傳輸量一半的時代,這些數字理應足以引發大規模採用。結果並沒有。
催生 WebP 的那場收購
WebP 不是來自圖像實驗室。它來自 視訊編解碼器。
2010 年 2 月,Google 收購了 On2 Technologies,作價約 1.24 億美元。On2 是一家影片壓縮公司,歷史悠久——他們的編解碼器驅動了 Flash video、Skype 視訊通話和 AOL 串流。他們的旗艦產品是 VP8,一款視訊編解碼器,目標是在免付專利權利金的情況下與 H.264 競爭。
Google 在 2010 年 5 月將 VP8 以 WebM 的名稱開源,搭配 Vorbis 音訊編解碼器與 Matroska 容器一起推出。目標很清楚:打造一套免專利費的影片技術棧,挑戰 MPEG-LA 的 H.264 授權池,當時這個授權池開始對網路影片串流收取權利金。
但 Google 還有第二個用途。VP8 的幀內壓縮——對單一影片影格進行壓縮,不參考其他影格——本質上就是一種靜態圖像編解碼器。VP8 用於影片的預測模式、變換編碼與熵編碼,同樣適用於照片。Google 提取了幀內模式,把它包進 RIFF 容器,命名為 WebP。
名稱是行銷決定。"Web" 因為它是為網路設計的。"P" 因為圖像格式都以 P 結尾——JPEG、PNG、BMP、TIFF。結果是一個技術上其實是影片影格假扮成照片的格式。
為什麼還要造新格式?
到了 2010 年,JPEG 已經十八歲,PNG 十四歲。兩者都已根深蒂固。何必多此一舉?
JPEG 的限制確實存在,而且眾所周知:
- 沒有透明度。JPEG 像素不是全不透明,就是需要獨立的遮罩。
- 沒有動畫。動畫 JPEG 並非標準。
- 只有有損壓縮。JPEG 的 baseline 規格沒有無損模式。(JPEG-LS 與 JPEG 2000 存在,但兩者都不適合網路。)
- 每通道 8-bit 色深。Baseline 不支援寬色域或 HDR。
- 低品質時的區塊假影。8 x 8 DCT 網格在品質設定低於 75 時清晰可見。
PNG 的限制同樣真實:
- 沒有有損模式。PNG 永遠是無損。一張 1200 萬畫素的照片轉成 PNG 是 15-25 MB。
- 照片檔案很大。PNG 的 DEFLATE 壓縮無法與基於 DCT 的心理視覺捨棄競爭。
- Base spec 沒有動畫。APNG 存在,但花了好幾年才獲得瀏覽器支援。
Google 看到一個缺口:一種格式能同時做到 有損與無損壓縮、透明度與動畫,而且檔案比現有格式更小。這就是 WebP 的賣點。
WebP 實際如何運作
WebP 有兩種根本不同的內部格式:有損 WebP(VP8 幀內模式)與 無損 WebP(獨立的編解碼器,同樣衍生自 VP8 研究)。
有損 WebP:VP8 Intra
有損 WebP 將 VP8 位元流存放在 RIFF 容器內。編碼流程概念上與 JPEG 類似,但有幾個關鍵差異:
| Stage | JPEG | 有損 WebP |
|---|---|---|
| Transform | 8 x 8 DCT | 4 x 4 或 16 x 16 integer DCT-like transform |
| Prediction | None (intra only) | 每個 4 x 4 block 有 4 種 intra-prediction 模式 |
| Chroma subsampling | 4:2:0 default | 4:2:0 default |
| 熵編碼 | Huffman | Binary arithmetic coding |
| Bit depth | 8-bit | 8-bit |
幀內預測 是最大的優勢。JPEG 獨立編碼每個 8 x 8 block。WebP 則從已編碼的鄰居——上方、左方或兩者——預測每個 4 x 4 block,然後只編碼預測誤差。對於平滑漸層與大面積平坦區域,誤差極小,壓縮比因此顯著提升。
算術編碼器也比 JPEG 的 Huffman coding 更有效率——在同樣品質下通常再省 5-10%。
Google 2010 年的自家基準測試宣稱:
| Metric | WebP vs JPEG |
|---|---|
| 平均檔案大小縮減 | 在同等 SSIM 下小 25-34% |
| 編碼速度 | 約比 libjpeg 慢 8 倍 |
| 解碼速度 | 與 libjpeg 相當 |
編碼速度是隱藏成本。產出 WebP 檔案耗費的 CPU 顯著高於 JPEG。對一次輸出數百張圖片的攝影師來說,這很重要。
無損 WebP
無損 WebP 使用完全不同的編解碼器。它不是 VP8。這是一種自訂格式,結合了:
- Predictive coding:每個像素有 14 種不同的空間預測模式。
- Color cache:最近出現過的顏色雜湊表,用來利用局部重複。
- LZ77 back-references:類似 PNG 的 DEFLATE,但具備 2D 空間感知匹配。
- Huffman + arithmetic hybrid:熵編碼會根據局部統計資料自動調整。
Google 宣稱平均比 PNG 小 26%。實務上,節省幅度差異很大——大面積平坦區域的簡單圖形幾乎沒有改善,而具有細緻紋理的照片可以縮小 30-40%。
Extended WebP(VP8X)
VP8X chunk 為 WebP 擴充了額外功能:
- Alpha channel:8-bit alpha 單獨編碼,以無損 WebP 的熵編碼器壓縮。
- Animation:多個影格搭配時間中繼資料,本質上是精簡版的 VP8 影片。
- EXIF metadata:相機與地理位置資料。
- XMP metadata:Adobe 風格的處理指令。
- ICC color profile:寬色域與 HDR 色彩管理。
VP8X 檔案以 VP8X chunk header 開頭,後接 flags,標示哪些擴充功能存在。
檔案格式
WebP 是一個 RIFF 容器。如果你懂 RIFF,理解 byte layout 很簡單。
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
若 bytes 12-15 的 FourCC 是 "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)
畫布尺寸以 width - 1 與 height - 1 儲存,所以一張 1200 x 675 的圖像會儲存 1199 與 674。最大畫布尺寸為 16,777,215 x 16,777,215 像素。
Chunk Types
| FourCC | Content | Compression |
|---|---|---|
VP8 | VP8 位元流(有損) | VP8 intra |
VP8L | VP8L 位元流(無損) | Custom lossless |
VP8X | 延伸標頭 + 旗標 | None |
ALPH | Alpha 通道資料 | Lossless WebP entropy |
ANMF | 動畫影格 | VP8/VP8L per frame |
ICCP | ICC 色彩描述檔 | None |
EXIF | EXIF 中繼資料 | None |
XMP | XMP 中繼資料 | None |
透過讀取 File Signature 辨識 WebP
別相信 .webp 副檔名。讀取前 16 個位元組並解析 RIFF header。
簡單有損 WebP 的精確 byte layout:
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:
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"}
使用 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
或者使用 webp 函式庫:
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"];
}
使用 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)
完整中繼資料提取:
magick identify -verbose image.webp
這會輸出寬度、高度、色深、alpha 是否存在、compression type 與 ICC profile 資訊。
或者簡單使用:
file image.webp
# image.webp: RIFF (little-endian) data, Web/P image
優勢
WebP 在特定面向技術上令人印象深刻:
更小的檔案:在 Google 的參考語料庫上,有損 WebP 在同等 SSIM 下平均比 JPEG 小 25-34%。無損 WebP 平均比 PNG 小 26%。對高流量網站而言,這些節省直接轉化為頻寬成本與更快的頁面載入。
功能整合:一種格式取代 JPEG 與 PNG 的大部分應用場景。有損模式用於照片,無損模式用於圖形,alpha channel 用於透明度,animation 用於短序列。網頁開發者只需要學一種格式,而非三種。
瀏覽器原生解碼:Chrome、Firefox、Safari 與 Edge 都內建硬體加速或高度最佳化的軟體 WebP 解碼器。解碼速度在桌面與 JPEG 相當,在手機上差距在 10-20% 以內。
漸進式解碼:WebP 支援資料抵達時逐漸顯示,類似 JPEG 的 progressive 模式。對慢速連線而言,收到約 30% 檔案後就能出現可辨識的圖像。
Animation:Animated WebP 檔案在同等視覺品質下通常比 animated GIF 小 60-80%,每個影格具備完整的 24-bit 色彩與 8-bit alpha。
弱點
WebP 的問題不在技術。它們是生態系的問題。
編碼速度:2010 年時,WebP 編碼大約比 libjpeg 慢 8 倍。差距已經縮小——2026 年的 libwebp 大約比 libjpeg-turbo 慢 2-3 倍——但對批次處理流程仍然重要。輸出 1000 張圖片的攝影師會明顯感覺到等待時間。
不支援 16-bit 或 HDR:WebP 嚴格限制在 8-bit per channel。對寬色域攝影、醫學影像或 HDR 內容來說,WebP 無法使用。HEIC、AVIF 與 JPEG XL 都支援更高的 bit depth。
無法無損重新壓縮 JPEG:JPEG XL 可以拿現有 JPEG 進行無損重新壓縮,節省約 20%。WebP 做不到。把 JPEG 轉成 WebP 需要完整重新編碼,這會引入 generation loss。
工具鏈缺口:Photoshop 直到 2022 年才原生支援 WebP。ImageMagick 的 WebP 支援需要編譯 libwebp 外掛,而許多發行版預設未包含。許多內容管理系統至今仍預設產生 JPEG/PNG。
VP8 的專利疑雲:Google 發布 VP8 時附帶了專利賠償承諾,但這個編解碼器的專利布局從未像 PNG 或 JPEG 那樣乾淨。有些組織刻意迴避 WebP,因為他們不信任 Google 的法律保護罩能在法庭上站得住腳。
為什麼「較差」的格式贏了
JPEG 三十四歲了。它沒有透明度、沒有動畫、沒有無損模式,品質 75 時就有明顯假影。WebP 幾乎在每項指標上都打敗它。然而 2025 年的 Web Almanac 顯示,JPEG 佔所有網路圖像約 46%,WebP 僅佔 19%。
原因不是技術。是 網路效應與轉換成本。
JPEG 是圖像格式的 QWERTY。每台相機預設存 JPEG。每支手機原生顯示 JPEG。每台印表機接受 JPEG。每個社群網路、CMS、CDN 與郵件客戶端都能無需外掛、編解碼器或轉換就處理 JPEG。這個格式如此普及,以至於對大多數使用者來說,「圖像」與「JPEG」幾乎是同義詞。
WebP 的採用曲線說明了這個故事:
| Year | Milestone |
|---|---|
| 2010 | Google 宣布 WebP(Chrome 8) |
| 2012 | Chrome 23 加入 lossless 與 alpha 支援 |
| 2013 | Chrome 加入 animated WebP |
| 2014 | Android 4.0+ 加入原生 WebP 支援 |
| 2015 | Facebook 將所有行動照片轉為 WebP |
| 2016 | Safari 14 加入 WebP 支援 |
| 2020 | 達成全面瀏覽器支援 |
| 2022 | Photoshop 加入原生 WebP 匯出 |
| 2025 | Web Almanac 顯示 WebP 佔網路圖像 19% |
Chrome 積極推動 WebP,因為 Google 同時掌控瀏覽器與格式。Facebook 採用它,因為這節省了數 PB 的頻寬。但網路的長尾——WordPress 部落格、小型電商網站、企業 CMS 部署、電子報——移動緩慢或根本沒動。
關鍵失敗點是 Apple 的生態系。iPhone 預設存 HEIC,不是 WebP。macOS Preview 直到 macOS 11 Big Sur(2020)才支援 WebP。iOS 分享選單沒有提供 WebP 匯出。對主要使用 Apple 裝置的攝影師、設計師與社群媒體創作者來說,WebP 形同不存在。
與此同時,AVIF 在 2019 年問世,壓縮率比 WebP 更好,而且由 Alliance for Open Media 提供免權利金授權。Chrome、Firefox 與 Safari 都已內建 AVIF。Cloudflare 與 Cloudinary 自動提供 AVIF。WebP 變成了墊腳石——比 JPEG 好,但已經被下一代超越。
WebP 現今的地位
WebP 不算失敗。它是 部分成功。
對 2026 年架設新專案的網頁開發者來說,WebP 是需要透明度或動畫的圖像的務實預設選擇。它比 PNG 更適合無損圖形,比 JPEG 更適合照片。瀏覽器支援已全面普及。編碼工具鏈也已成熟。
但 WebP 沒有取代 JPEG。它只是在 JPEG 旁邊切出一個利基——PNG 本來就佔據的那個利基,只是檔案更小。「一種格式統治所有圖像」的願景並未實現。
2026 年的實務現況:
| 應用場景 | 最佳格式 | 原因 |
|---|---|---|
| 照片(既有) | JPEG | 全面普及,encode 快,夠小了 |
| 照片(新建) | AVIF | 比 WebP 小 30%,免權利金 |
| 照片(備援) | WebP | 比 JPEG 小 25%,支援全面 |
| 無損圖形 | WebP 或 PNG | WebP 較小;PNG 是安全備援 |
| 透明度 | WebP 或 PNG | WebP 檔案較小;PNG 是安全備援 |
| 動畫 | WebP 或 AVIF | 兩者都比 GIF 小 60-80%;AVIF 較新 |
| 寬色域 / HDR | AVIF 或 JPEG XL | 10+ bit depth,ICC/ICC v4 支援 |
| 印刷流程 | TIFF 或 JPEG XL | CMYK、16-bit、lossless JPEG 重新壓縮 |
WebP 真正的遺產在於證明了網路確實可以在大型瀏覽器廠商強力推動時採用新圖像格式。它為 AVIF 鋪路。它迫使 Apple 原生支援 JPEG/PNG 以外的格式。它證明了透明度與動畫應該放在單一容器裡。
但它也證明了 技術優越性並不足夠。普及度、慣性與生態系整合的重要性高於壓縮比。JPEG 會比 WebP 活得久,不是因為它更好,而是因為它已經無所不在。
結論
WebP 是從視訊編解碼器打造出來的,目的是解決頻寬問題。它確實解決了——對 Google、對 Facebook、對任何願意轉換圖像流程的網站而言。壓縮是真的,功能有用,瀏覽器支援也完整。
但網路不會因為白皮書說新格式小了 25% 就換格式。它只在兩種情況下轉換:新格式比舊格式更容易使用,或舊格式爛到非遷移不可。JPEG 沒有爛到那個程度。WebP 也不夠容易。而等到 WebP 變得容易時,AVIF 已經帶著規格表上更大的數字登場了。
WebP 是圖像格式的 Betamax——技術紮實,支援完善,最終被稍晚出現、行銷更好、後盾更廣的東西超越。它不會消失。它會與 JPEG、PNG、AVIF 以及未來的任何格式共存,扮演與 PNG 今日相同的角色:那個安全、可靠、到處都能用的備援格式。
如果你有需要在網路上縮小體積的 PNG 檔案,PNG to WebP 可以在你的瀏覽器本地轉換——無需上傳,無需伺服器處理。對於需要透明度或動畫的 JPEG,JPG to WebP 可以處理轉換並控制品質。而當你需要通用備援格式時,WebP to PNG 與 WebP to JPG 可以把 WebP 檔案轉回能在每個檢視器開啟的格式。



