The Picture Element with JPG Fallback
The HTML <picture> element is the most reliable way to serve WebP with a fallback for older browsers. It lets the browser choose the best available format without any JavaScript:
<picture>
<source srcset="hero.webp" type="image/webp">
<source srcset="hero.jpg" type="image/jpeg">
<img src="hero.jpg" alt="Hero banner" width="1200" height="630"
loading="lazy" decoding="async">
</picture>
When the browser supports WebP, it downloads hero.webp. Otherwise, it falls back to hero.jpg. The <img> tag serves as the ultimate fallback and also holds the alt, width, height, and loading attributes.
For responsive images, combine the <picture> element with srcset and sizes to serve different resolutions based on viewport width:
<picture>
<source
srcset="photo-400.webp 400w, photo-800.webp 800w, photo-1200.webp 1200w"
sizes="(max-width: 600px) 100vw, (max-width: 1200px) 50vw, 600px"
type="image/webp">
<img
srcset="photo-400.jpg 400w, photo-800.jpg 800w, photo-1200.jpg 1200w"
sizes="(max-width: 600px) 100vw, (max-width: 1200px) 50vw, 600px"
src="photo-800.jpg" alt="Product photo" width="800" height="600"
loading="lazy" decoding="async">
</picture>
Do you still need a JPG fallback? In 2026, over 99% of browsers support WebP. If your analytics show zero Internet Explorer traffic, you can skip the fallback and serve WebP directly in a standard <img> tag. The <picture> element is still useful if you want to serve AVIF as a first choice with WebP as fallback.
Server-Side Content Negotiation
Content negotiation checks the browser's Accept header and serves WebP transparently — without any HTML changes. When a browser requests an image and includes image/webp in its Accept header, the server responds with the WebP version.
Nginx Configuration
Add this to your Nginx server block to serve WebP variants when they exist alongside the original files:
map $http_accept $webp_suffix {
default "";
"~*webp" ".webp";
}
server {
location ~* \.(jpe?g|png)$ {
add_header Vary Accept;
try_files $uri$webp_suffix $uri =404;
}
}
This checks for a .webp file alongside the original (e.g., photo.jpg.webp next to photo.jpg). If the browser supports WebP and the file exists, Nginx serves it. The Vary: Accept header ensures CDNs and proxy caches store separate versions for different browser capabilities.
Apache .htaccess Rules
For Apache servers, add these rewrite rules to your .htaccess file:
RewriteEngine On
RewriteCond %{HTTP_ACCEPT} image/webp
RewriteCond %{REQUEST_FILENAME}.webp -f
RewriteRule ^(.+)\.(jpe?g|png)$ $1.$2.webp [T=image/webp,L]
Header append Vary Accept env=REDIRECT_accept
The same principle applies: if the browser accepts WebP and a .webp variant exists on disk, Apache serves it transparently. No HTML changes required.
CDN Auto-Conversion Solutions
The easiest approach for most websites is to let your CDN handle WebP conversion automatically. You upload JPG/PNG originals, and the CDN serves optimized WebP to supported browsers with zero code changes.
| CDN / Service | Feature | Setup |
|---|---|---|
| Cloudflare Polish | Auto WebP + compression | One-click toggle (Pro plan+) |
| Cloudinary | Auto-format via f_auto | Add f_auto to image URL |
| imgix | Auto-format via auto=format | URL parameter |
| AWS CloudFront | Lambda@Edge conversion | Custom Lambda function |
| Vercel / Netlify | Built-in image optimization | Automatic with Image component |
CDN-based solutions handle content negotiation, caching, and cache invalidation automatically. They are the recommended approach for dynamic websites where users or content editors upload images — you cannot predict which images will appear, so automatic conversion is essential.
WordPress WebP Setup
WordPress 5.8+ supports WebP uploads natively, but you need a plugin to convert existing images and serve them optimally:
- ShortPixel Image Optimizer: Converts existing images to WebP on the fly. Serves WebP via
<picture>element or .htaccess rewrite. Free tier: 100 images/month - Imagify: Bulk conversion with three quality levels (normal, aggressive, ultra). Integrates with WP Rocket for caching. Free tier: 20 MB/month
- EWWW Image Optimizer: Generates WebP copies automatically on upload. Rewrites image URLs via plugin or CDN. Free unlimited local optimization
- WebP Express: Lightweight plugin focused solely on WebP conversion. Configures .htaccess or uses PHP-based serving. Free and open source
For most WordPress sites, ShortPixel or EWWW are the safest choices. They handle the complete pipeline: convert on upload, generate WebP variants, rewrite HTML to serve WebP, and fall back to JPG/PNG for any unsupported browser.
Build Tool Integration
For static sites and JavaScript frameworks (React, Vue, Next.js, Nuxt), integrate WebP generation into your build pipeline so images are converted automatically at build time.
Webpack (image-minimizer-webpack-plugin)
const ImageMinimizerPlugin = require("image-minimizer-webpack-plugin");
module.exports = {
optimization: {
minimizer: [
new ImageMinimizerPlugin({
generator: [{
preset: "webp",
implementation: ImageMinimizerPlugin.sharpGenerate,
options: { encodeOptions: { webp: { quality: 80 } } }
}]
})
]
}
};
Vite (vite-plugin-imagemin)
import viteImagemin from "vite-plugin-imagemin";
export default defineConfig({
plugins: [
viteImagemin({
webp: { quality: 80 }
})
]
});
Build-time conversion works well for sites with a known set of images (marketing pages, documentation, blogs). For user-uploaded content, prefer CDN-based solutions that process images dynamically.
Best Quality Settings
Choosing the right WebP quality setting is critical. Too low and you get visible artifacts; too high and you lose the file size benefits. Here are recommended settings by content type:
| Content Type | Recommended Quality | Notes |
|---|---|---|
| Photos (products, portraits) | 75–85 | Best balance of quality and size |
| Hero banners / marketing | 80–90 | Higher quality for large, prominent images |
| Thumbnails / previews | 70–80 | Small display size hides artifacts |
| Graphics / illustrations | 80–90 | Flat color areas are more sensitive to artifacts |
| Screenshots / text-heavy | Lossless | Text quality degrades noticeably with lossy compression |
Starting point: Quality 80 is the sweet spot for most photographic content. It produces visually indistinguishable results from JPG quality 85 at roughly 30% smaller file size. Test with your specific images and adjust from there.
Measuring the Impact
After implementing WebP, measure the actual performance improvement with these tools:
- Google Lighthouse: Run audits before and after the switch. Check the "Serve images in next-gen formats" diagnostic — it should disappear. Compare Performance score, LCP, and Total Blocking Time
- PageSpeed Insights: Tests both mobile and desktop from Google's servers. The "Opportunities" section should no longer list image optimization as a recommendation
- WebPageTest: Provides detailed waterfall charts showing individual image load times. Compare filmstrip views before and after to see visual rendering speed differences
- Chrome DevTools Network tab: Filter by "Img" type and compare total transfer size. Verify the
Content-Typeheader showsimage/webpfor converted images - Search Console Core Web Vitals: Monitor field data over 2–4 weeks after the switch. Look for improvements in LCP and overall "Good URLs" percentage
Lazy Loading Considerations
WebP and lazy loading work together to maximize performance, but there are important implementation details:
- Above-the-fold images: Do NOT lazy-load your hero image or any image visible on initial page load. Use
loading="eager"(the default) and addfetchpriority="high"for the LCP image - Below-the-fold images: Use
loading="lazy"on all other images. The browser defers loading until the user scrolls near them - Always set dimensions: Include explicit
widthandheightattributes on every<img>tag to prevent Cumulative Layout Shift (CLS) - Decode asynchronously: Add
decoding="async"to allow the browser to decode images off the main thread
<!-- Hero image: eager loading, high priority -->
<img src="hero.webp" alt="..." width="1200" height="630"
fetchpriority="high" decoding="async">
<!-- Below-the-fold: lazy loading -->
<img src="product.webp" alt="..." width="600" height="400"
loading="lazy" decoding="async">