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 — which sits under AVIF vs WebP Compression Benchmarks 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.

Browser format negotiation cascade for the picture element Flowchart showing how Chrome 85+, Safari 16+ select AVIF; Safari 14–15 select WebP; and Safari 13 / Edge 18 fall back to JPEG via the img element. Browser sends Accept: image/avif, image/webp, */* Browser evaluates <picture> sources Supports image/avif? Yes Serve hero.avif Chrome 85+, Safari 16+, FF 93+ No Supports image/webp? Yes Serve hero.webp Safari 14–15, Edge 18+ No Serve hero.jpg Safari 13, legacy browsers

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.

The difference the header makes is structural, not cosmetic: it changes how many objects the edge stores for a single URL. Without it the edge keeps one object per URL and the first requester’s format wins for everyone routed to that node.

Edge cache partitioning with and without Vary: Accept Two panels. Without Vary Accept the edge stores one object per URL, so a cached AVIF is served to Safari 14 and renders as a broken image. With Vary Accept the edge keys on the Accept header and stores one object per browser tier, so every tier receives a decodable file. WITHOUT Vary: Accept Chrome 121 Accept: image/avif Safari 14 Accept: image/webp Edge cache node cache key = URL only 1 object: hero.avif Safari 14 is served hero.avif broken image icon, wasted download WITH Vary: Accept Chrome 121 Accept: image/avif Safari 14 Accept: image/webp object A: hero.avif keyed on image/avif object B: hero.webp keyed on image/webp cache key = URL + Accept Each tier receives a decodable file no poisoning, +10-15% edge hit rate add_header Vary Accept always; belongs on every location block that negotiates format

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

Plotted against a common axis, the three percentage rows show where the win actually comes from: the payload reduction is the largest single movement, LCP follows it closely, and the cache-hit gain is a smaller but permanent structural improvement rather than a per-request saving.

Metric deltas after deploying the format cascade Horizontal range bars on a percentage axis: LCP falls 18 to 32 percent, total image payload falls 22 to 40 percent, and CDN cache hit rate rises 10 to 15 percent. Typical deltas measured after the AVIF - WebP - JPEG cascade ships LCP (mobile 4G) Total image payload CDN cache hit rate -18% to -32% -22% to -40% +10% to +15% -40% -30% -20% -10% 0 +10% +20% Not plotted: Safari 14 fallback TTFB overhead, which stays under 50 ms. Each bar spans the observed range; all three point in the improving direction.

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">

6. Declaring srcset densities only on the <img>

Anti-pattern: Adding a full responsive srcset/sizes pair to the <img> but leaving each <source> with a single fixed URL.

Effect: The <img> attributes are only consulted when no <source> matches. A Chrome client that matches the AVIF source therefore receives the one fixed-width AVIF regardless of viewport or device pixel ratio, while Safari 13 — the tier least able to afford it — gets the properly sized responsive JPEG. The result is that your best-supported browsers download the worst-fitting file.

Fix: Repeat srcset (and sizes, if the layout is not full-bleed) on every <source> element. Each source carries its own candidate list; the browser first picks the source by type, then runs the normal selection algorithm inside it. See mastering srcset and sizes for responsive layouts for the width-descriptor arithmetic.

Edge cases worth knowing

Safari 16.0 decodes still AVIF but not animated AVIF. Animated AVIF landed in Safari 16.4. If your fallback chain includes animated sources, treat 16.0–16.3 as a separate tier: serve animated WebP or a muted looping <video>, and keep animated AVIF behind a capability check rather than a version check.

Wide-gamut AVIF can shift colour in the WebP tier. AVIF carries 10-bit and Display-P3 content natively; WebP is 8-bit and sRGB only. When your AVIF master is P3, convert — do not merely truncate — to sRGB when generating the WebP variant, or the Safari 14 tier will render visibly desaturated versions of the same art.

Service worker caches can defeat Vary. The Cache API honours Vary by default, but cache.match(request, { ignoreVary: true }) disables it. That single option reproduces the exact edge-cache poisoning this guide prevents, only inside the browser where no CDN log will show it.

Some CDNs normalise Accept before it reaches your origin. Fastly, for example, is usually configured to collapse the header into a small number of buckets so the cache does not fragment across thousands of unique Accept strings — see normalising the Accept header for AVIF in Fastly VCL. If your origin logic tests for an exact substring, confirm the normalised value still contains it.