How to Serve WebP Images with a JPG Fallback: The Complete picture Element Guide

WebP is supported by 97%+ of browsers in 2026 — but "almost all" is not "all." Outlook does not render WebP. Some WhatsApp versions do not load WebP previews. Old iOS devices (pre-iOS 14) cannot display WebP. For these edge cases, you need a fallback. The good news: the HTML <picture> element makes this entirely automatic, with zero JavaScript, and zero extra downloads for modern browsers.

Why WebP Still Needs a Fallback (in Some Cases)

For images served inside a browser to a modern audience, a fallback is no longer strictly necessary. But WebP breaks silently in several non-browser environments that still matter:

  • Email clients: Microsoft Outlook (desktop), some versions of Apple Mail, and Thunderbird do not render WebP. Always use JPG for images embedded in email campaigns.
  • Social media scrapers: The og:image meta tag must point to a JPG or PNG. Facebook, Twitter/X, LinkedIn, and WhatsApp do not support WebP for Open Graph link previews — serving WebP here produces broken or missing preview cards.
  • Old iOS: Safari on iOS 13 and earlier (released 2019) does not support WebP. While the installed base is small and shrinking, enterprise or education audiences may have older devices.
  • Windows Photo Viewer (pre-2018 Windows): The legacy Windows photo viewer cannot open WebP files. Users who download your images may be unable to view them.
  • For website images served in a browser: No fallback is needed. WebP coverage is effectively universal at 97%+.

The conclusion: if your images stay inside a browser, skip the fallback complexity. If your images leave the browser — through email, social sharing, or direct download — keep JPG versions alongside your WebP files.

The HTML picture Element — How It Works

The <picture> element is the browser-native solution for serving different image formats without JavaScript. The basic pattern is straightforward:

<picture>
  <source srcset="photo.webp" type="image/webp">
  <img src="photo.jpg" alt="Product photo" width="800" height="600">
</picture>

Here is exactly what happens when this markup is parsed:

  • The browser reads <source> elements from top to bottom.
  • If it supports type="image/webp", it downloads photo.webp and stops — no further sources are evaluated.
  • If it does not support WebP, it falls through to the <img> tag and downloads photo.jpg.
  • Only one file is ever downloaded — there is no wasted bandwidth fetching both.

The <img> tag is mandatory inside <picture>. It serves two roles: it is the fallback for browsers that match no <source>, and it is the element that carries alt, width, height, loading, and all CSS styling. Style the <img>, not the <picture> wrapper.

Adding Responsive Images to the Mix (srcset + sizes)

The <picture> element handles format selection. The srcset and sizes attributes handle responsive sizing. You can combine both on each <source> element to get format fallback and responsive dimensions in a single block of markup:

<picture>
  <source
    type="image/webp"
    srcset="photo-400.webp 400w, photo-800.webp 800w, photo-1200.webp 1200w"
    sizes="(max-width: 600px) 100vw, 800px">
  <source
    srcset="photo-400.jpg 400w, photo-800.jpg 800w, photo-1200.jpg 1200w"
    sizes="(max-width: 600px) 100vw, 800px">
  <img src="photo-800.jpg" alt="Product" width="800" height="600" loading="lazy">
</picture>

How this works in practice:

  • srcset lists image candidates at different pixel widths (400w, 800w, 1200w). The browser picks the most appropriate size based on the device's screen width and pixel density.
  • sizes tells the browser how wide the image will be rendered so it can select the right candidate before the CSS loads. (max-width: 600px) 100vw, 800px means: on screens up to 600px wide, the image fills the full viewport; on larger screens, it is displayed at 800px.
  • A WebP-capable browser picks from the first <source>; older browsers fall through to the JPG <source>; both get responsive sizing.

Lazy Loading with picture

The loading="lazy" attribute goes on the <img> tag — it applies to the entire <picture> element, including all its sources. This defers downloading the image until it enters (or is near) the viewport:

<picture>
  <source srcset="hero.webp" type="image/webp">
  <img src="hero.jpg" alt="Hero" width="1200" height="630" loading="lazy">
</picture>

Use loading="lazy" on all images that appear below the fold. For the primary above-the-fold image (your hero banner, article lead image, or LCP element), omit loading="lazy" or explicitly set loading="eager", and add fetchpriority="high" to tell the browser this image is critical:

<picture>
  <source srcset="hero.webp" type="image/webp">
  <img src="hero.jpg" alt="Hero" width="1200" height="630"
       loading="eager" fetchpriority="high">
</picture>

Lazy-loading your LCP image is one of the most common performance mistakes — it delays the metric that matters most to Google's Core Web Vitals score.

Convert your JPG images to WebP — free, browser-based

Convert your JPG images to WebP instantly — browser-based, no upload, free.

Server-Side Automatic WebP: Nginx

If you want to serve WebP automatically without changing any HTML, you can configure your web server to detect the browser's Accept header and swap in the WebP version. This approach requires pre-generated .webp versions of all your images stored on disk alongside the originals.

Add the following to your Nginx configuration:

# In your http or server block, add a map:
map $http_accept $webp_suffix {
    default   "";
    "~*webp"  ".webp";
}

server {
    location ~* \.(jpe?g|png)$ {
        # Try the WebP version first, fall back to original
        add_header Vary Accept;
        try_files $uri$webp_suffix $uri =404;
    }
}

When a browser sends Accept: image/webp, Nginx appends .webp to the requested filename and tries to serve photo.jpg.webp. If that file exists on disk, it is served with the correct content type. If it does not exist, Nginx falls back to the original JPG. The Vary: Accept header is essential — it tells CDNs and proxies to cache the WebP and JPG responses separately, so a cached WebP response is not served to a browser that requested JPG.

Server-Side Automatic WebP: Apache

The Apache equivalent uses mod_rewrite. Add the following to your .htaccess file:

<IfModule mod_rewrite.c>
  RewriteEngine On

  # Check if browser supports WebP
  RewriteCond %{HTTP_ACCEPT} image/webp

  # Check if a .webp version of the image exists
  RewriteCond %{DOCUMENT_ROOT}/$1.webp -f

  # Serve the .webp version
  RewriteRule ^(.*)\.(jpe?g|png)$ $1.webp [T=image/webp,E=accept:1,L]
</IfModule>

<IfModule mod_headers.c>
  Header append Vary Accept env=REDIRECT_accept
</IfModule>

This ruleset only rewrites the request when two conditions are both true: the browser's Accept header includes image/webp, and a .webp file exists in the document root at the same path. If either condition fails, the original JPEG or PNG is served unchanged. Like the Nginx approach, the Vary: Accept header ensures correct CDN caching behaviour.

Cloudflare Polish — Zero-Config Automatic WebP

For most sites, Cloudflare Polish is the easiest path to automatic WebP delivery. It requires no changes to your HTML, server configuration, or image files. Here is how it works:

  1. Cloudflare intercepts every image request at the edge before it reaches your origin server.
  2. It checks the browser's Accept header — specifically whether it includes image/webp.
  3. If the browser accepts WebP, Cloudflare converts the image on the fly and serves a cached WebP copy from the nearest edge location.
  4. Subsequent requests for the same image are served from the Cloudflare cache — no conversion overhead, no origin hits.
  5. No code changes to your HTML are required. Your <img> tags continue to reference .jpg or .png files.

To enable Polish: Cloudflare Dashboard → Speed → Optimization → Polish → choose Lossy (recommended for photos — same visual quality, smaller files) or Lossless (for graphics with sharp edges or transparency). Polish is available on paid Cloudflare plans.

React / Next.js Implementation

In a React application without a framework image component, a reusable component keeps the <picture> pattern DRY:

function OptimizedImage({ src, alt, width, height }) {
  const webpSrc = src.replace(/\.(jpe?g|png)$/i, '.webp');
  return (
    <picture>
      <source srcSet={webpSrc} type="image/webp" />
      <img src={src} alt={alt} width={width} height={height} loading="lazy" />
    </picture>
  );
}

This component assumes you have pre-generated .webp versions of each image at the same path with a different extension. Pass the original JPG or PNG path as src — the component derives the WebP path automatically.

In Next.js, the built-in Image component handles WebP delivery automatically at the server level — no <picture> element needed in your JSX:

import Image from 'next/image';

// next/image automatically serves WebP or AVIF based on the browser's
// Accept header. No picture element needed — Next.js handles format
// negotiation at the server level via its image optimization API.
<Image src="/photo.jpg" alt="Product" width={800} height={600} />

Next.js generates multiple format variants and sizes on first request, caches them, and serves each browser the most efficient format it supports — WebP for modern browsers, JPEG for older ones. It also sets correct srcset, sizes, and lazy loading attributes automatically.

Testing Your Setup

After implementing any of the above approaches, verify it is working correctly:

  • DevTools Network tab: Open Chrome DevTools → Network → filter by "Img". For each image request, check the "Type" column — it should show webp for WebP-capable browsers.
  • Response headers: Click any image request and check the Response Headers panel. You should see Content-Type: image/webp and, for server-side approaches, Vary: Accept.
  • Throttled connection test: In DevTools → Network → set throttle to "Slow 3G". Reload the page. You should see WebP files loading, not JPG — confirming the browser selected WebP regardless of connection speed.
  • Fallback test: In Chrome, navigate to chrome://flags and search for "WebP". Disable WebP image format support, relaunch Chrome, and reload your page. The Network tab should now show JPG or PNG being served — confirming the fallback works.
  • CDN cache validation: If using a CDN, make two requests to the same image URL — one with Accept: image/webp in the headers, one without. Both should return the correct format, confirming the CDN is keying its cache on the Vary: Accept header.

The og:image Exception — Keep Social Preview Images as JPG

This is the most commonly overlooked gotcha when rolling out WebP site-wide: your <meta property="og:image"> tag must point to a JPG or PNG, never a WebP.

Here is why: when someone shares your URL on Facebook, Twitter/X, LinkedIn, WhatsApp, or Slack, the platform's scraper fetches the og:image URL to generate a link preview card. These scrapers are not web browsers — they do not send Accept: image/webp, and many do not support WebP at all. If you serve WebP as your og:image, the result is a broken or missing image in the link preview.

The correct pattern is to have two versions of your key images:

  • og-cover.jpg (or .png) — referenced in your <meta property="og:image"> tag
  • og-cover.webp — served to browsers via the <picture> element for on-page display
<!-- In your <head> — always JPG or PNG for social scrapers -->
<meta property="og:image" content="https://example.com/images/cover.jpg">

<!-- In your article body — WebP for browsers, JPG fallback -->
<picture>
  <source srcset="images/cover.webp" type="image/webp">
  <img src="images/cover.jpg" alt="Cover image" width="1200" height="630">
</picture>

If your Nginx or Apache configuration automatically rewrites JPG URLs to WebP when the requester accepts WebP — and a social scraper requests your og:image URL without Accept: image/webp — the rewrite rules will correctly serve JPG to the scraper. This scenario is handled safely. The risk only arises if you explicitly point og:image at a .webp URL.

Frequently Asked Questions

Do I still need a JPG fallback for WebP in 2026?
For most websites targeting browser traffic, no — WebP has 97%+ browser support. The main exceptions: email clients (Outlook, Apple Mail), WhatsApp link previews, some image editing software. If images will be shared outside a browser, keep JPG versions.
Does using picture hurt performance vs a plain img tag?
No. The browser only downloads one source — the first matching source element. No extra network request is made for unsupported sources. The <picture> element adds zero overhead to supported browsers and saves bandwidth by delivering a smaller WebP file instead of a larger JPG.
How does Cloudflare Polish serve WebP automatically?
Cloudflare Polish rewrites image URLs on the fly: when a browser sends Accept: image/webp in its request headers, Cloudflare serves a converted WebP copy from its edge cache. No code change is required — just enable Polish in the Cloudflare Speed settings. The original files on your origin server are untouched; Cloudflare handles conversion and caching at the edge.
Can I use srcset with WebP inside a picture element?
Yes — and you should. Combine <picture> (for format fallback) with srcset and sizes (for responsive dimensions) on each <source> element. This gives format selection AND responsive sizing in one markup pattern. A WebP-capable browser picks the right WebP size for its viewport; an older browser picks the right JPG size. Both get responsive images; both get the correct format.
What about og:image — should it be WebP?
No. Keep og:image as JPG or PNG. Facebook, Twitter/X, LinkedIn and most social scrapers do not support WebP for Open Graph images. Serving WebP as og:image results in broken or missing previews when your links are shared on social media. Keep a JPG version of your social preview images regardless of your on-page format strategy.
Convertlo Editorial Team
The Convertlo team writes practical guides on image formats, file conversion, and web performance. All technical content is verified against current format specifications and tested in real browsers.
convertlo.pro