How to configure AVIF fallbacks for Safari 14
Safari 14 shipped native WebP decoding but excluded AVIF — that support arrived only in Safari 16. If you serve AVIF unconditionally, Safari 14 and 15 users receive an image they cannot decode: a broken icon or a silent download. This guide — part of the AVIF vs WebP Compression Benchmarks cluster within Core Media Fundamentals & Next-Gen Formats — shows the exact HTML structure, Nginx location block, and CDN header setup that lets each browser receive the best format it supports, with no cache collisions between tiers.
Prerequisite checklist
Before applying the fallback chain, confirm each item is in place:
How the browser cascade works
The <picture> element evaluates <source> children top-to-bottom and stops at the first type the browser reports as supported. Safari 14 reports image/webp support but not image/avif, so it skips the first source and matches the second. Safari 13 and IE skip both and render the <img> fallback. Crucially, format selection happens entirely in the client — the server must therefore serve whichever file the browser requests, not force a single format at the origin.
Exact solution
Step 1 — HTML <picture> fallback structure
<picture>
<!--
AVIF source: evaluated first.
Supported by Safari 16+, Chrome 85+, Firefox 93+.
The `type` attribute is the gate — browsers that do not
declare image/avif support skip this source silently.
-->
<source srcset="/img/hero.avif" type="image/avif">
<!--
WebP source: fallback for Safari 14–15 and older Chromium.
Safari 14 added image/webp support in its Accept header;
any browser that skipped AVIF and supports WebP matches here.
-->
<source srcset="/img/hero.webp" type="image/webp">
<!--
Universal fallback img element.
width/height prevent Cumulative Layout Shift (CLS) while
the chosen format downloads.
fetchpriority="high" applies to ALL browsers — it signals
high priority regardless of which source wins the cascade.
Use only on the single above-fold LCP candidate; applying
fetchpriority="high" to multiple images starves CSS and
other critical resources.
-->
<img src="/img/hero.jpg"
alt="Descriptive alt text for the hero image"
width="800"
height="450"
loading="eager"
fetchpriority="high">
</picture>
Warning: Do not add loading="lazy" to the <img> when the image is the LCP candidate. Lazy loading defers the fetch, which directly increases LCP time. Reserve loading="lazy" for below-fold images.
Step 2 — Nginx origin configuration
location ~* \.(avif|webp|jpg|jpeg|png)$ {
# Vary: Accept is the critical CDN instruction.
# Without it, a CDN node may cache hero.avif from the first
# request and serve it to Safari 14 on all subsequent requests,
# producing a broken image. This header tells every RFC-7234
# compliant cache to store separate objects per Accept value.
add_header Vary Accept always;
# Explicit MIME mapping prevents fallback to
# application/octet-stream when the browser inspects
# Content-Type before decoding.
types {
image/avif avif;
image/webp webp;
image/jpeg jpg jpeg;
image/png png;
}
# Immutable cache with fingerprinted filenames.
# max-age=31536000 = 1 year; immutable tells supporting browsers
# (Firefox, Safari 14+) to skip revalidation on reload.
# Only safe with content-addressed URLs (e.g. hero.a3f9c2.jpg).
add_header Cache-Control "public, max-age=31536000, immutable" always;
expires 1y;
try_files $uri =404;
}
Step 3 — CDN-level Vary forwarding
Cloudflare strips Vary: Accept by default on free and pro plans and instead uses its own image format routing via Polish or Image Resizing. If you are using a plain CDN pass-through, verify that Vary: Accept is preserved in edge responses:
# Confirm Vary header reaches the client through the CDN
curl -sI https://yourdomain.com/img/hero.jpg | grep -i vary
# Expected: Vary: Accept
If your CDN normalises the Accept header into the cache key rather than forwarding Vary, you may need to configure a cache rule. On Cloudflare, use a Cache Rule that includes Accept in the cache key, or enable Polish with AVIF output to let Cloudflare handle format negotiation at the edge.
Verification steps
1. Check AVIF delivery for modern browsers
# Simulate Chrome 85+ / Safari 16+ Accept header.
# Expected Content-Type: image/avif
curl -sI \
-H 'Accept: image/avif,image/webp,image/*,*/*;q=0.8' \
https://yourdomain.com/img/hero.jpg \
| grep -i 'content-type'
2. Check WebP fallback for Safari 14
# Simulate Safari 14 Accept header (image/webp, no image/avif).
# Expected Content-Type: image/webp
curl -sI \
-H 'Accept: image/webp,image/*,*/*;q=0.8' \
https://yourdomain.com/img/hero.jpg \
| grep -i 'content-type'
3. Verify Vary header is present
# Must return "Vary: Accept" for CDN caches to store separate
# objects per browser tier. Absence causes cache poisoning.
curl -sI https://yourdomain.com/img/hero.jpg | grep -i vary
4. Measure LCP impact with Lighthouse CLI
# Run Lighthouse against a mobile emulation profile.
# Compare largest-contentful-paint before and after rollout.
lighthouse https://yourdomain.com \
--only-categories=performance \
--output=json \
| jq '.audits["largest-contentful-paint"].displayValue'
Expected metric deltas after correct deployment:
| Metric | Typical delta | Notes |
|---|---|---|
| LCP | −18% to −32% vs baseline JPEG | Only if fetchpriority="high" is set and the preload hint is present |
| Total image payload | −22% to −40% | AVIF typically reduces file size 15–20% beyond WebP at equal SSIM |
| CDN cache hit rate | +10–15% | Correct Vary: Accept prevents per-request origin misses |
| Safari 14 fallback TTFB overhead | <50 ms | WebKit’s WebP decoder initialises quickly; the overhead is negligible |
Common mistakes and fixes
1. Omitting Vary: Accept on the origin
Anti-pattern: The Nginx config serves AVIF and WebP correctly but never emits Vary: Accept.
Effect: The first browser to hit a CDN node caches its format. Every subsequent visitor on that node receives the same file regardless of their Accept header. Safari 14 users get image/avif and display a broken icon.
Fix: Add add_header Vary Accept always; to every location block that performs content negotiation. The always flag ensures the header appears on non-2xx responses too, preventing cache poisoning on 304 redirects.
2. Missing type attribute on <source>
Anti-pattern:
<source srcset="/img/hero.avif">
<source srcset="/img/hero.webp">
Effect: Without type, the browser cannot evaluate format support. It fetches the first <source> regardless of decode capability. Safari 14 downloads hero.avif, fails to decode it, and shows a broken image — with a wasted network request.
Fix: Always include type="image/avif" and type="image/webp" on the respective <source> elements. The browser uses this attribute to gate the request before fetching.
3. Applying fetchpriority="high" to multiple images
Anti-pattern: Adding fetchpriority="high" to the hero, a product thumbnail, and a logo simultaneously.
Effect: The browser’s preload scanner treats all three as equally critical, which floods the high-priority fetch queue. CSS, fonts, and render-blocking scripts are starved, increasing First Contentful Paint (FCP) and Time to Interactive (TTI). See preload vs prefetch for video and image assets for the full priority model.
Fix: Reserve fetchpriority="high" for the single confirmed LCP candidate. Use fetchpriority="low" or omit the attribute on all other images above the fold.
4. Wrong AVIF MIME type registration
Anti-pattern: Using image/x-avif or no MIME entry at all for .avif files.
Effect: The browser receives Content-Type: application/octet-stream or Content-Type: image/x-avif. Even browsers with full AVIF support may refuse to render the image or trigger a download dialog.
Fix: Register exactly image/avif avif; in your Nginx types block. On Apache, add AddType image/avif .avif to the relevant <Directory> or .htaccess. Confirm registration with curl -sI and inspect Content-Type.
5. Preloading AVIF without a format hint
Anti-pattern:
<link rel="preload" as="image" href="/img/hero.avif">
Effect: Safari 14 receives a preload hint for a file it cannot decode. It fetches the AVIF eagerly, then discards it and later fetches the WebP, doubling the image payload for that browser tier. This increases LCP for exactly the users who need the most help.
Fix: Scope preloads by MIME type using imagesrcset with a JS-guarded snippet, or use a <link> with type="image/avif" — which modern browsers honour while Safari 14 ignores:
<!--
type="image/avif" causes Safari 14 to ignore this preload.
Only Chrome 85+ and Safari 16+ act on it.
Eliminates the wasted AVIF fetch on WebP-only browsers.
-->
<link rel="preload" as="image" type="image/avif" href="/img/hero.avif">
Related
- AVIF vs WebP Compression Benchmarks — file-size and SSIM data for choosing between formats
- When to use WebP over JPEG in production — decision matrix and fallback workflows for the WebP/JPEG tier
- MIME type configuration for modern media servers — registering
image/avifandimage/webpcorrectly on Nginx, Apache, and Node.js - Cache-Control headers for image and video assets — setting
max-age,immutable, andVaryto maximise CDN efficiency