WebP and “image compression” get used interchangeably, but they solve two different problems. Compressing a JPEG at quality 70 makes the file smaller. Converting that same image to WebP makes the file smaller and keeps more visual detail at the same byte count. One is a setting; the other is a format change. This tutorial walks through the format change end to end, in the order you would implement it on a real project.
Checklist: Before You Touch a Single File
Work through these first. Skipping them is the most common reason a WebP migration stalls halfway.
- Confirm your hosting or CDN serves WebP with the correct
Content-Type. A file ending in.webpserved asimage/jpegwill be rejected by browsers even though it looks fine on disk. - Decide your fallback strategy. WebP is supported by all current major browsers, but if your audience includes very old clients, you still need a JPEG or PNG fallback.
- Pick your conversion tool.
cwebp(CLI),sharp(Node),Pillow(Python), or a build-plugin likevite-imagetools. The CLI is the fastest way to learn what the knobs do. - Identify your largest images by rendered size, not file size. A 200 KB hero image displayed at 1600px wide matters more than a 400 KB decorative image shown at 200px. This is about Largest Contentful Paint (LCP), not total bytes.
- Decide lossy vs. lossless per image type. Photographs → lossy. Screenshots, UI mockups, and images with text or sharp edges → consider lossless or a high quality setting.
If a checklist item is unresolved, resolve it before proceeding. The rest of this guide assumes all five are settled.
Symptom → Cause → Fix: The Four Things That Go Wrong
Symptom 1: “The WebP file is larger than the original JPEG.”
Cause: You converted a source that was already heavily optimized, or you exported a lossless WebP from a photographic source. Lossless WebP on a photo can easily produce a file 2-3x the size of a well-encoded JPEG, because lossless compression has no way to discard the imperceptible detail a photo contains.
Fix: Use lossy mode for photographs. cwebp defaults to lossy, so if you see a size increase, check whether you passed -lossless. Also check that your source image isn’t already a re-encoded JPEG at quality 60 — the encoder has less to work with.
# Typical lossy conversion for a photograph.
# -q 75 is a reasonable starting point; -m 6 enables the slowest, best compression.
cwebp -q 75 -m 6 -metadata none source.jpg -o source.webp
-metadata none strips EXIF and ICC data. That is usually correct for web delivery, but do not strip metadata from images that depend on an embedded color profile, or colors will shift.
Symptom 2: “The image renders blank, broken, or as a download prompt.”
Cause: Almost always a MIME type mismatch. The server is returning Content-Type: image/jpeg (or nothing at all) for a .webp file.
Fix: Check what your server sends:
curl -sI https://example.com/images/hero.webp | grep -i content-type
You want content-type: image/webp. If your nginx config or CDN rules do not have this mapping, add it. On nginx:
types {
image/webp webp;
}
Most managed hosts and CDNs handle this automatically. Self-hosted setups and some older object storage buckets do not.
Symptom 3: “LCP got worse after converting to WebP.”
Cause: You converted the images but never set width and height, or you dropped the srcset. The format change reduced bytes, but layout shift and incorrect sizing now cost more than the bytes saved. A browser that has to download a 1600px-wide WebP to display it at 400px is still wasting bandwidth, WebP or not.
Fix: Serve multiple sizes and let the browser pick. See the <picture>/srcset pattern in the next section.
Symptom 4: “Some users see an old image after deployment.”
Cause: Browser or CDN caching on the original URL. WebP files converted in place often keep the same path, so caches serve stale content.
Fix: Version the filename (hero.a1b2c3.webp) or use a content hash generated by your build tool. Do not rely on cache-busting query strings alone — some CDNs are configured to ignore them.
Concrete Implementation Path
Step 1: Setup — convert a batch
Install cwebp (part of libwebp-tools on Debian/Ubuntu, webp on Homebrew), then run it across a directory:
# Convert every JPEG in ./images to WebP at quality 78, keeping originals.
for f in images/*.jpg; do
cwebp -q 78 -m 6 -metadata none "$f" -o "${f%.jpg}.webp"
done
For a project with a build step, a Node-based approach integrates more cleanly. With sharp:
import sharp from "sharp";
import { readdir } from "node:fs/promises";
import path from "node:path";
const dir = "./images";
const files = await readdir(dir);
for (const file of files.filter(f => /\.(jpe?g|png)$/i.test(f))) {
const src = path.join(dir, file);
const out = path.join(dir, file.replace(/\.(jpe?g|png)$/i, ".webp"));
await sharp(src)
.resize({ width: 1600, withoutEnlargement: true })
.webp({ quality: 78, effort: 6 })
.toFile(out);
console.log(`wrote ${out}`);
}
The effort option is the sharp equivalent of cwebp’s -m. Higher values take longer to encode but produce smaller files. Anything above 6 has diminishing returns for most content.
Step 2: Change — serve the right file to the right browser
The safest markup keeps the original as a fallback and lets the browser choose WebP when it can decode it:
<picture>
<source
type="image/webp"
srcset="/images/hero-800.webp 800w,
/images/hero-1600.webp 1600w"
sizes="(max-width: 800px) 100vw, 800px">
<img
src="/images/hero-1600.jpg"
width="1600"
height="900"
alt="Product dashboard view"
loading="eager"
fetchpriority="high">
</picture>
Three details matter here. The type="image/webp" attribute tells the browser to skip the source entirely if WebP is unsupported, so it never downloads a file it cannot decode. The width and height attributes prevent layout shift. And fetchpriority="high" on an LCP element is what tells the browser this image outranks other resources in the queue.
For images below the fold, swap loading="eager" for loading="lazy". Do not lazy-load your LCP image — it is a common cause of worse LCP after an “optimization” pass.
Step 3: Verify — confirm the browser is receiving WebP
After deploying, check that the browser is fetching the WebP variant, not the fallback. In Chrome or Firefox, open DevTools → Network, filter by Img, and look at the Type column. It should read webp for images you converted. If it reads jpeg, either the type attribute is missing, the server is sending the wrong MIME type, or you are looking at a cached response.
A quick command-line sanity check:
curl -sI -H "Accept: image/webp,*/*" https://example.com/images/hero-1600.webp
Then confirm the returned file is a valid WebP:
file hero-1600.webp
# hero-1600.webp: RIFF (little-endian) data, Web/P image
If file reports JPEG or PNG, the conversion step silently failed — usually because the source path was wrong and an old file was left in place.
Trade-offs and When Not to Use WebP
WebP is a strong default, but it is not always the right answer.
- AVIF or JPEG XL may be better for very large photographic images. AVIF commonly achieves 15-30% smaller files than WebP at equal quality, though encoding is slower and older browser support is spotter. For a hero image on a marketing page, it is worth testing.
- Lossless WebP is usually the wrong tool. For screenshots, PNG with a proper palette quantization tool (like
pngquant) or a vector format often wins on both size and fidelity. - Do not convert small icons. A 2 KB PNG converted to a 1.8 KB WebP is not worth the added
<picture>markup and the extra request logic. Reserve the format change for images above roughly 20-30 KB. - Skip WebP if your pipeline already serves AVIF with a WebP fallback. Nesting three
<source>elements adds real maintenance cost for marginal gain. - Watch out for alpha channels. If you export a transparent PNG to lossy WebP, the alpha plane is preserved but quality on the alpha edge can degrade visibly. Use lossless or verify against the PNG before replacing it.
As a rough rule of thumb, well-encoded lossy WebP on photographic content typically lands 25-40% smaller than an equivalent-quality JPEG. The exact figure depends on the source image and the quality setting, so treat any single number as illustrative rather than guaranteed.
A Short Post-Migration Checklist
- Server sends
Content-Type: image/webpfor.webpfiles. - Every converted image has a working fallback for unsupported browsers.
- LCP images have explicit
width,height, andfetchpriority="high". - Below-the-fold images use
loading="lazy". - Filenames are content-hashed or versioned so caches do not serve stale files.
- A spot check in DevTools confirms the network panel shows
webp.
Run through this after every batch conversion. One missed MIME mapping can quietly undo weeks of optimization work, and the only sign will be a browser silently falling back to the original JPEG.
🔗 Recommended Reading
- Core Web Vitals Optimization for Finance and Banking Websites: A Troubleshooting Checklist
- Common Core Web Vitals Mistakes Developers Make (And How to Fix Them)
- Diagnosing and Fixing Common Lighthouse Audit Mistakes
- Browser Caching Setup: A Beginner's Step-by-Step Guide
- Troubleshooting Slow Server Response Times: Common Mistakes and Fixes