WebP #
Images are the biggest contributor to total web page size — on average more than 50% of the total bytes transferred. Choosing the right image format and compressing it correctly is one of the most impactful web performance optimizations, and also one of the most often ignored. WebP is a format developed by Google offering better compression than both JPEG and PNG for almost every use case — photos, graphics, transparency, and animation. This article covers how WebP works, concrete comparisons with other formats, how to use it with proper fallbacks, automated conversion pipelines, and when another format might actually be more appropriate.
Why Image Formats Matter for Performance #
Before discussing WebP, it’s important to understand the context of why this matters:
The impact of images on web performance:
The average web page (2024):
Total size: ~2.5 MB
Images: ~1.2 MB (48% of the total!)
JavaScript: ~600 KB
CSS: ~100 KB
HTML: ~50 KB
Unoptimized images = the biggest problem source:
JPEG product photo 2000x2000px: 800 KB - 2 MB
Equivalent WebP: 200 KB - 500 KB (~60-75% smaller!)
Implications for Core Web Vitals:
LCP (Largest Contentful Paint) — often a hero image or product image
→ Large, slow-downloading images = poor LCP
→ Smaller WebP = faster LCP
Image Format Comparison #
flowchart LR
subgraph Formats["Image Format Comparison"]
direction TB
JPEG["JPEG\nPhotos and gradients\nLossy only\nNo transparency\nSupport: universal"]
PNG["PNG\nGraphics and transparency\nLossless only\nLarge sizes\nSupport: universal"]
WebP["WebP\nAll use cases\nLossy and Lossless\nTransparency and animation\nSupport: 95%+ browsers"]
AVIF["AVIF\nBest compression\nLossy and Lossless\nTransparency and animation\nSupport: ~90% browsers"]
GIF["GIF\nSimple animation\nOnly 256 colors\nVery large sizes\nSupport: universal"]
endConcrete size comparison — an e-commerce product image 1200x900px:
Format Size Quality Transparency Animation
JPEG 280 KB ★★★★☆ ✗ ✗
PNG 850 KB ★★★★★ ✓ ✗
GIF 2.1 MB ★★☆☆☆ ✓ (1-bit) ✓
WebP 165 KB ★★★★☆ ✓ ✓
AVIF 120 KB ★★★★★ ✓ ✓
Conclusion:
→ WebP is 41% smaller than JPEG, with comparable quality
→ WebP is 81% smaller than PNG for the same image
→ AVIF is even better, but browser support is still more limited
→ GIF can almost always be replaced with animated WebP or video
Lossy vs Lossless in WebP #
WebP supports two modes that are often confusing:
Lossy mode (like JPEG, but more efficient):
→ Some image data is discarded to achieve smaller sizes
→ The higher the compression, the more detail is lost
→ Quality 80 is usually the sweet spot: small size, still good quality
→ Use for: photos, hero images, product photos, backgrounds
cwebp -q 80 photo.jpg -o photo.webp
→ q=80 means quality 80 (not 80% compression)
→ 0 = worst quality, smallest size
→ 100 = best quality, largest size
Lossless mode (like PNG, but more efficient):
→ All image data is preserved — pixel perfect
→ Larger than lossy but smaller than PNG
→ Use for: logos, icons, UI screenshots, images with text
→ Transparency (alpha channel) is supported in this mode too
cwebp -lossless logo.png -o logo.webp
Transparent WebP:
→ Can be lossy for color pixels + lossless for the alpha channel
→ Or fully lossless
→ Replaces transparent PNGs
cwebp -q 80 -alpha_q 100 icon_transparent.png -o icon.webp
HTML Implementation — <picture> and Fallbacks
#
Although WebP is supported by 95%+ of modern browsers, fallbacks still matter for older browsers (especially Safari before 2020, and IE).
<!-- The correct way: <picture> with a fallback -->
<picture>
<!-- The browser picks the first format it supports -->
<source srcset="product.avif" type="image/avif"> <!-- best, ~90% support -->
<source srcset="product.webp" type="image/webp"> <!-- good, 95%+ support -->
<img src="product.jpg" <!-- universal fallback -->
alt="Gaming Laptop X1"
width="800"
height="600"
loading="lazy">
</picture>
<!-- The wrong way: using WebP directly without a fallback -->
<img src="product.webp" alt="Gaming Laptop X1">
<!-- Older browsers that don't support WebP: the image doesn't appear! -->
<!-- Responsive images + WebP + fallback -->
<picture>
<source
type="image/avif"
srcset="product-400.avif 400w,
product-800.avif 800w,
product-1200.avif 1200w"
sizes="(max-width: 600px) 400px,
(max-width: 1200px) 800px,
1200px"
>
<source
type="image/webp"
srcset="product-400.webp 400w,
product-800.webp 800w,
product-1200.webp 1200w"
sizes="(max-width: 600px) 400px,
(max-width: 1200px) 800px,
1200px"
>
<img
src="product-800.jpg"
srcset="product-400.jpg 400w,
product-800.jpg 800w,
product-1200.jpg 1200w"
sizes="(max-width: 600px) 400px,
(max-width: 1200px) 800px,
1200px"
alt="Gaming Laptop X1"
width="800"
height="600"
loading="lazy"
decoding="async"
>
</picture>
<!-- The browser does:
1. Checks: do I support AVIF? If yes → pick the right size from the AVIF sources
2. If not → Checks: do I support WebP? If yes → pick the WebP size
3. If not → falls back to regular JPEG
All automatic! -->
Lazy Loading Images #
Images not visible in the viewport when the page loads don’t need immediate downloading.
<!-- Native HTML lazy loading (supported by all modern browsers) -->
<img
src="product.jpg"
loading="lazy" <!-- only loads when nearing the viewport -->
decoding="async" <!-- decodes in the background, doesn't block the main thread -->
width="800" <!-- REQUIRED: prevents layout shifts (CLS) -->
height="600" <!-- REQUIRED: browsers know dimensions before the image loads -->
alt="Product X"
>
<!-- Don't lazy load above-the-fold images (hero images, logos) -->
<!-- Lazy loading a hero image actually slows down LCP! -->
<img
src="hero.jpg"
loading="eager" <!-- default, no need to write it -->
fetchpriority="high" <!-- tells the browser this is high priority -->
alt="Hero image"
>
Lazy loading guidance:
Images that SHOULD be lazy loaded:
→ Product listings (many images, many below the fold)
→ Gallery images
→ Avatars in comment sections
→ Related article thumbnails
→ All images below the initial viewport
Images that must NOT be lazy loaded:
→ Hero/banner images (above the fold)
→ Header logos
→ The first carousel image
→ Images immediately visible when the page opens
Why width and height are mandatory:
Without explicit dimensions:
Browsers don't know how much space is needed before the image downloads
→ Content shifts when images appear → high CLS
With explicit dimensions:
Browsers reserve the right space in advance
→ No shifting → low CLS
→ Even when display sizes differ (responsive), the aspect ratio is preserved
Conversion in Build Pipelines #
Converting images manually isn’t scalable. An automated pipeline is the correct way to handle this.
// Go — automatic conversion in the build pipeline
// (equivalent of the Vite plugin — run this step during the build)
// main.go
func convertImages(assetsDir string) error {
entries, err := os.ReadDir(assetsDir)
if err != nil {
return err
}
for _, entry := range entries {
ext := strings.ToLower(filepath.Ext(entry.Name()))
if ext != ".jpg" && ext != ".png" {
continue
}
in := filepath.Join(assetsDir, entry.Name())
out := strings.TrimSuffix(in, ext) + ".webp"
if err := exec.Command("cwebp", "-q", "80", in, "-o", out).Run(); err != nil {
return err // webp: quality 80
}
}
return nil
}
# CLI conversion with cwebp (Google WebP tools)
# Single file conversion
cwebp -q 80 input.jpg -o output.webp
# Lossless conversion (for transparent PNGs)
cwebp -lossless input.png -o output.webp
# Batch conversion in Bash — all JPEGs in a folder
for f in images/*.jpg; do
cwebp -q 80 "$f" -o "${f%.jpg}.webp"
done
# With ImageMagick — more flexible for batches
mogrify -format webp -quality 80 images/*.jpg
mogrify -format webp -define webp:lossless=true images/*.png
Content Negotiation — Serving the Right Format from the Server #
A more elegant approach is letting the server decide which format to send based on the browser’s Accept header.
How Content Negotiation works:
Modern browsers send:
Accept: image/avif,image/webp,image/apng,*/*;q=0.8
Older browsers send:
Accept: image/png,image/jpeg,*/*;q=0.8
The server reads the Accept header and:
→ If the browser supports AVIF: send the AVIF version
→ If the browser supports WebP: send the WebP version
→ Fallback: send the original JPEG/PNG
# Nginx — Content Negotiation for WebP
map $http_accept $webp_suffix {
default "";
"~*webp" ".webp";
}
server {
location ~* \.(png|jpe?g)$ {
# Check whether a WebP version of this image exists
add_header Vary Accept; # important for correct CDN caching!
try_files $uri$webp_suffix $uri =404;
# Aggressive caching for images
expires 1y;
add_header Cache-Control "public, immutable";
}
}
# How it works:
# Request: GET /product.jpg
# Accept header: image/webp,*/*
# Server looks for: /product.jpg.webp
# If found: sends product.jpg.webp (content-type: image/jpeg stays!)
# If not found: sends the regular product.jpg
// Go — Content Negotiation middleware
func serveImage(w http.ResponseWriter, r *http.Request) {
imagePath := r.URL.Path // e.g. /images/product.jpg
acceptHeader := r.Header.Get("Accept")
// Check browser support
supportsAVIF := strings.Contains(acceptHeader, "image/avif")
supportsWebP := strings.Contains(acceptHeader, "image/webp")
var filePath string
var contentType string
switch {
case supportsAVIF:
avifPath := strings.TrimSuffix(imagePath, filepath.Ext(imagePath)) + ".avif"
if fileExists(avifPath) {
filePath = avifPath
contentType = "image/avif"
}
case supportsWebP:
webpPath := strings.TrimSuffix(imagePath, filepath.Ext(imagePath)) + ".webp"
if fileExists(webpPath) {
filePath = webpPath
contentType = "image/webp"
}
}
// Fall back to the original file
if filePath == "" {
filePath = imagePath
contentType = mime.TypeByExtension(filepath.Ext(imagePath))
}
w.Header().Set("Content-Type", contentType)
w.Header().Set("Vary", "Accept") // important for CDN caching!
http.ServeFile(w, r, filePath)
}
Animated WebP vs GIF vs Video #
GIF is a very inefficient format for animation — it only supports 256 colors and has very large sizes. Animated WebP is a much better replacement, but video (MP4/WebM) is even better for long animations.
<!-- GIF — the old way, inefficient -->
<img src="animation.gif" alt="Animation demo">
<!-- A 5-second 800x600 GIF: can reach 5-15 MB! -->
<!-- Animated WebP — much smaller -->
<picture>
<source srcset="animation.webp" type="image/webp">
<img src="animation.gif" alt="Animation demo"> <!-- fallback -->
</picture>
<!-- The same animated WebP: ~1-3 MB -->
<!-- Video — most efficient for long animations -->
<video autoplay loop muted playsinline>
<source src="animation.webm" type="video/webm">
<source src="animation.mp4" type="video/mp4">
</video>
<!-- The same video: ~200-500 KB! -->
<!-- When to use which:
Short (< 3 seconds), simple animation: animated WebP
Long or complex animation: video (mp4/webm)
GIF: almost no reason to use it on the modern web -->
WebP Anti-Patterns to Avoid #
Using WebP Without a Fallback #
<!-- ✗ Anti-pattern: WebP directly without a fallback -->
<img src="logo.webp" alt="Logo">
<!-- Safari < 14, IE, and some older mobile browsers: the image doesn't appear! -->
<!-- ✓ Solution: Always provide a fallback with <picture> -->
<picture>
<source srcset="logo.webp" type="image/webp">
<img src="logo.png" alt="Logo">
</picture>
Ignoring width and height
#
<!-- ✗ Anti-pattern: no dimensions -->
<img src="product.webp" alt="Product" loading="lazy">
<!-- Browsers don't know how much space to reserve
→ Content shifts when the image loads → high CLS! -->
<!-- ✓ Solution: always include width and height -->
<img
src="product.webp"
alt="Product"
width="400"
height="300"
loading="lazy"
>
Lazy Loading Hero Images #
<!-- ✗ Anti-pattern: lazy loading above-the-fold images -->
<img src="hero.webp" alt="Hero" loading="lazy">
<!-- The image that should be seen first actually loads later
→ LCP worsens! -->
<!-- ✓ Solution: eager + fetchpriority for hero images -->
<img src="hero.webp" alt="Hero" fetchpriority="high">
No Quality Optimization #
# ✗ Anti-pattern: converting without quality settings
cwebp input.jpg -o output.webp
# Default quality = 75 — may be fine, but not optimal
# ✗ Or: quality too high
cwebp -q 100 input.jpg -o output.webp
# Quality 100 = large files, minimal WebP benefit
# ✓ Solution: find the sweet spot per image type
# Photos: quality 75-85
cwebp -q 80 photo.jpg -o photo.webp
# Images with text/UI: quality 85-90 (text needs to be sharper)
cwebp -q 90 ui_screenshot.jpg -o ui_screenshot.webp
# Small thumbnails: quality 70-75
cwebp -q 70 thumbnail.jpg -o thumbnail.webp
Image Format Selection Guide #
flowchart TD
Q1{"Image content\ntype?"}
Q2{"Need\ntransparency?"}
Q3{"Animation\nor video?"}
Q4{"Need\npixel-perfect\nquality?"}
Foto["Photo / Hero Image\n→ WebP lossy (q=80)\nFallback: JPEG"]
Logo["Logo / Icon\n→ SVG if possible\nor WebP lossless\nFallback: PNG"]
TransFoto["Photo with\ntransparency\n→ WebP lossy+alpha\nFallback: PNG"]
Anim["Short animation\n→ animated WebP\nFallback: GIF"]
Vid["Long animation\n→ MP4/WebM video\n(not an image!)"]
Screenshot["Screenshot / UI\n→ WebP lossless\nor PNG"]
Q1 -->|"Photo"| Q2
Q1 -->|"Logo / Icon"| Logo
Q1 -->|"Animation"| Q3
Q1 -->|"Screenshot"| Q4
Q2 -->|"No"| Foto
Q2 -->|"Yes"| TransFoto
Q3 -->|"Short < 3s"| Anim
Q3 -->|"Long / complex"| Vid
Q4 -->|"No"| Screenshot
Q4 -->|"Yes (pixel art, etc.)"| Screenshot
style Foto fill:#27AE60,color:#fff
style Logo fill:#2980B9,color:#fff
style TransFoto fill:#E67E22,color:#fff
style Anim fill:#8E44AD,color:#fff
style Vid fill:#27AE60,color:#fff
style Screenshot fill:#2980B9,color:#fffWeb Image Checklist #
FORMAT:
□ Photos and hero images use lossy WebP (quality 75-85)
□ Logos and icons: SVG when possible, WebP lossless otherwise
□ Transparent images: WebP with an alpha channel
□ Animation: animated WebP for short, video for long
□ <picture> with fallbacks for all non-universal WebP
DIMENSIONS AND SIZES:
□ width and height attributes on all <img> (prevents CLS)
□ Images no larger than displayed (responsive sizes)
□ srcset with multiple resolutions for important images
□ Hero images available in several sizes (mobile, tablet, desktop)
LOADING:
□ loading="lazy" for below-the-fold images
□ fetchpriority="high" for hero images / LCP elements
□ decoding="async" for non-critical-path images
PIPELINE:
□ Automatic conversion via build tools (Vite, webpack, Next.js)
□ Or CDN image optimization (Cloudflare, imgix, Cloudinary)
□ Quality settings configured (not defaults)
□ Original images kept as the source of truth (re-generatable)
CACHING:
□ Cache-Control immutable for images with content hashes in URLs
□ Vary: Accept header for server-side content negotiation
□ CDN configured to serve WebP automatically when supported
Summary #
- WebP is on average 25-35% smaller than JPEG — and supports transparency (like PNG) plus animation (like GIF), making it a versatile format that should be the default for almost all web images.
- Always use
<picture> with fallbacks — WebP is supported by 95%+ of modern browsers, but Safari before 2020 and some older mobile browsers don’t support it. <picture> with JPEG/PNG fallbacks ensures all users get images. width and height are mandatory on every <img> — without them, browsers can’t reserve space before images download, causing bad layout shifts (CLS) when images appear.- Lazy load below-the-fold images, eager load hero images —
loading="lazy" is very effective for galleries and product listings. But don’t lazy load hero images — it actually worsens LCP. - Automated conversion pipelines are the right way — manual image conversion isn’t scalable. Use build tools (Vite, webpack imagemin), built-in framework features (Next.js Image), or CDN image optimization.
- Quality 80 is the sweet spot for photos — indistinguishable to the naked eye, but far smaller than quality 100. Small photos/thumbnails can use quality 70-75.
- Content Negotiation via the Accept header — servers can decide which format to send based on the browser’s Accept header, without needing
<picture> in the HTML. Nginx and modern CDNs support this. - Add the
Vary: Accept header — when using Content Negotiation, this header tells CDNs that responses differ by Accept header. Without it, CDNs may cache the WebP version and serve it to browsers that don’t support WebP. - GIF can almost always be replaced — animated WebP for short animations, video (MP4/WebM) for long ones. Video is far smaller than GIF for the same content.
- AVIF is the next step after WebP — ~20% better compression than WebP, supported by ~90% of browsers. Use it as the first choice in
<picture> with WebP as the second fallback.
#
- WebP is on average 25-35% smaller than JPEG — and supports transparency (like PNG) plus animation (like GIF), making it a versatile format that should be the default for almost all web images.
- Always use
<picture>with fallbacks — WebP is supported by 95%+ of modern browsers, but Safari before 2020 and some older mobile browsers don’t support it.<picture>with JPEG/PNG fallbacks ensures all users get images. widthandheightare mandatory on every<img>— without them, browsers can’t reserve space before images download, causing bad layout shifts (CLS) when images appear.- Lazy load below-the-fold images, eager load hero images —
loading="lazy"is very effective for galleries and product listings. But don’t lazy load hero images — it actually worsens LCP. - Automated conversion pipelines are the right way — manual image conversion isn’t scalable. Use build tools (Vite, webpack imagemin), built-in framework features (Next.js Image), or CDN image optimization.
- Quality 80 is the sweet spot for photos — indistinguishable to the naked eye, but far smaller than quality 100. Small photos/thumbnails can use quality 70-75.
- Content Negotiation via the Accept header — servers can decide which format to send based on the browser’s Accept header, without needing
<picture>in the HTML. Nginx and modern CDNs support this. - Add the
Vary: Acceptheader — when using Content Negotiation, this header tells CDNs that responses differ by Accept header. Without it, CDNs may cache the WebP version and serve it to browsers that don’t support WebP. - GIF can almost always be replaced — animated WebP for short animations, video (MP4/WebM) for long ones. Video is far smaller than GIF for the same content.
- AVIF is the next step after WebP — ~20% better compression than WebP, supported by ~90% of browsers. Use it as the first choice in
<picture>with WebP as the second fallback.