How to Fix a Slow LCP Image Served Through an Image CDN

This walkthrough extends Image CDNs and fetchpriority within Image & Media Optimization. You moved the hero image behind an image CDN, the file is now 62KB of AVIF instead of 310KB of JPEG, and LCP got worse. The bytes are not the problem. The first request for a given variant is doing work — a fetch from origin, a decode, a re-encode — while the browser waits.

Rapid Diagnosis: Where Is the Time Going?

  • Split the request timing. In the Network panel, expand the image's Timing tab. A large "Waiting for server response" with a small "Content Download" means transform latency, not transfer.
  • Check the cache status header. Most image CDNs expose a hit/miss header. A MISS on your hero image on every deploy means the variant is not warm.
  • Look for a redirect. A 302 from your origin to the CDN adds a full round trip before the image request even starts.
  • Count the variants. If the URL includes a viewport-derived width, you may be generating dozens of near-identical variants, none of which stays warm.
Cold transform versus warm hit for the LCP image On a cache miss the image CDN spends most of the time transforming before any bytes are sent; on a hit the same variant returns almost immediately, so the download dominates. Small file, slow arrival Cold (MISS) connect origin fetch + decode + re-encode 62KB 1.9s Warm (HIT) connect wait 62KB 0.5s Your LCP image must be a HIT for the first visitor after every deploy — not the request that warms it.

Root Cause Analysis

1. Transform-on-miss latency. The first request for a variant triggers an origin fetch plus a decode and re-encode. AVIF encoding in particular is expensive, and a large source image can take a second or more.

2. A redirect hop. Serving the image from your own origin and redirecting to the CDN costs a full round trip at high priority, directly in front of the LCP element.

3. Variant explosion. URLs built from the actual viewport width produce a distinct variant per device width. Each one is cold on first use, and the rarest widths are never warm for anyone.

4. A missing preconnect. The image CDN is a separate origin. Without a hint, the DNS lookup and TLS handshake happen at the moment the LCP image is discovered, adding 100–300ms on mobile.

5. The variant changes on every deploy. If the URL includes a build hash or a changing signature, yesterday's warm cache is worthless today.

Step-by-Step Resolution

1. Pin a small set of widths and warm them at deploy.

bash
# Warm exactly the variants the site can request, right after deploy.
for w in 640 960 1280 1600 2400; do
  for fmt in avif webp; do
    curl -s -o /dev/null -H "Accept: image/$fmt" \
      "https://img.example-cdn.com/hero.jpg?w=$w&fm=$fmt&q=70" &
  done
done; wait
# trade-off: warming costs transform capacity at deploy time and only helps the
# variants you listed. Keep the ladder short — five widths covers virtually
# every device once srcset is doing its job.

Expected outcome: the first real visitor after a deploy gets a cache hit, removing 0.5–1.5s of transform latency from LCP.

2. Remove the redirect. Reference the CDN URL directly in the markup rather than routing through your origin.

html
<img src="https://img.example-cdn.com/hero.jpg?w=1600&fm=avif&q=70"
     srcset="https://img.example-cdn.com/hero.jpg?w=960&fm=avif&q=70 960w,
             https://img.example-cdn.com/hero.jpg?w=1600&fm=avif&q=70 1600w"
     sizes="(min-width: 1200px) 1100px, 100vw"
     width="1600" height="900" alt="Coastal trail at sunrise"
     fetchpriority="high" decoding="async">
<!-- trade-off: hard-coding the CDN host couples your markup to the vendor.
     Emit it from one config value so a migration is a single change, not a
     find-and-replace across templates. -->

Expected outcome: one round trip removed from the critical path.

3. Preconnect the image origin. The hint must appear before the image is discovered, in the document head.

html
<link rel="preconnect" href="https://img.example-cdn.com">
<!-- trade-off: preconnect slots are scarce — two or three total. If the image
     CDN shares an origin with other assets you already preconnect, this hint
     is free; if not, it is worth one of the slots for the LCP resource. -->

4. Keep the variant URL stable. Derive it from the source asset and the ladder, never from a build hash or a per-request signature. If your CDN requires signed URLs, use a long-lived signature so the cache key does not rotate on every deploy.

Verification

Deploy, then request the hero variant twice and compare. The first request should already be a hit if warming worked; if the first is a miss and the second is fast, the warm step ran against a different URL than the page uses — a very common off-by-one in the query parameters.

bash
# Confirm the exact URL the page requests is warm.
url=$(curl -s https://example.com | grep -o 'https://img\.example-cdn\.com[^" ]*w=1600[^" ]*' | head -1)
for i in 1 2; do
  curl -s -o /dev/null -w '%{time_starttransfer}s  %{http_code}  ' "$url"
  curl -sI "$url" | grep -i '^\(x-cache\|cf-cache-status\|x-served-by\):'
done
# trade-off: shell extraction of the URL is fragile against HTML changes. It is
# still the right check, because the whole class of bug is warming a URL that
# differs from the one in the markup.

Then confirm the metric. LCP's resource-load-delay and load-time phases should both shrink — delay from the preconnect and the removed redirect, load time from the warm transform. If only bytes improved and the timestamp did not, the transform was never the bottleneck and you should re-read the phase breakdown.

FAQ

Should the LCP image be lazy-loaded if it is behind a CDN?

No. Lazy-loading defers discovery until layout, which is the opposite of what the LCP element needs regardless of who serves it. Keep the hero eager, give it fetchpriority="high", and reserve lazy loading for images below the fold — the rules in lazy loading images without hurting LCP apply unchanged.

How many width variants should I generate?

Four to six. Each additional width divides your cache warmth without meaningfully improving the byte fit, since srcset already picks the nearest candidate above the required size. A ladder like 640, 960, 1280, 1600 and 2400 covers essentially every device, and every one of those variants can be kept warm.

Is AVIF still the right format for the hero?

It depends on decode cost for your audience, not on file size alone. AVIF's smaller bytes are a clear win when the device can decode quickly, and a net loss on low-end hardware where decode lands inside LCP render delay. Measure both on your target device profile — the trade-off is worked through in AVIF vs WebP.

Should I warm variants for every page or only key templates?

Only where a cold transform lands on the critical path — the hero of your highest-traffic templates, and any campaign landing page shipped with new imagery. Warming the entire catalogue costs transform capacity for images most visitors will never request, and a below-the-fold image absorbing a transform delay is invisible. Rank by traffic multiplied by whether the image is the LCP candidate, and warm the top of that list.

What quality setting should the LCP image use?

Lower than you think, and verified on a real screen rather than chosen by number. Perceptual quality flattens out well below the defaults most CDNs ship — for a photographic hero, a setting around 60–70 is usually indistinguishable from 85 while shipping a third fewer bytes. Compare the two at the actual rendered size on a high-DPI display, not zoomed in: artefacts that are obvious at 400% are invisible at 100%.

Can I keep the transform on my own origin instead?

You can, and it trades one problem for another. Transforming at your origin removes a connection and gives you full control of caching, but it puts image encoding on infrastructure that is usually sized for HTML, and a cold hero transform now competes with request handling. The pattern that works is to pre-generate the fixed variant ladder at build time and serve the results as static assets — no runtime transform anywhere, and every variant permanently warm.