fetchpriority=high Not Working on Your LCP Image

You added fetchpriority="high" to the hero image, yet DevTools still shows it starting more than a second into the load — or sitting at Low priority — and LCP has not moved.

Root cause: the hint is either never seen, never applied, or applied too late

fetchpriority is a passive attribute with no error channel. When it works, the request shifts bands; when it does not, nothing warns you — no console message, no DevTools flag, no Lighthouse complaint about a malformed hint. The failure is always one of three silences, and diagnosing which one you have determines the fix.

The browser never sees the attribute. The hint travels as literal markup, and anything that mangles the markup kills it. The HTML attribute must be exactly fetchpriority (all lowercase) with a value of highfetchPriority="high" happens to work in HTML because attribute names are case-insensitive there, but the same camelCase string set as a DOM expando (img.fetchpriority = 'high') is inert, since the reflected property is fetchPriority. Templating layers introduce a second variant of this failure: an attribute placed on a wrapping <picture> or <a> element instead of the <img> itself does nothing, because the hint is only defined on the element that owns the fetch. The third variant is infrastructure: HTML-optimizing CDN features and “performance rewriter” proxies re-serialize markup and have a documented history of dropping attributes they do not recognise, so the attribute you see in your repository may never reach the browser.

The request the hint would apply to does not exist at parse time. The attribute participates in scheduling only when the preload scanner or parser creates the image request from markup. If the hero is painted via a CSS background-image, assembled by a client-side framework after hydration, or swapped in from a data-src by a lazy-loading library, there is no early request to promote — the fetch is created whenever script finally runs, and by then the priority band it lands in barely matters because 800–1,500 ms of discovery delay is already sunk. A perfectly-formed hint on a late-discovered image is the most common “it’s not working” report, and it is technically working — it is just irrelevant.

Another attribute overrides the intent. loading="lazy" on the same element wins outright: the browser withholds the request until the element approaches the viewport, so the high hint applies to a fetch that starts after layout, after scroll evaluation, after everything you were trying to beat. Boilerplates and CMS image plugins that stamp loading="lazy" onto every image are the usual source. A related override: a <link rel="preload"> for the same URL without the attribute fires first at the preload default (Low for images) and the element-level hint never gets a chance to schedule anything.

Minimal reproduction

All five failure modes, condensed. Each broken line looks plausible in review:

<!-- BROKEN 1: camelCase DOM expando — the reflected property is
     fetchPriority; this line creates an inert property and no hint -->
<script>
  const hero = document.querySelector('#hero');
  hero.fetchpriority = 'high'; // silently does nothing
</script>

<!-- BROKEN 2: hint on the wrapper — picture does not own the fetch -->
<picture fetchpriority="high">
  <img id="hero" src="/img/campaign.avif" width="1600" height="900" alt="Campaign hero">
</picture>

<!-- BROKEN 3: lazy wins — the request is withheld until viewport
     proximity, so the priority band is moot -->
<img src="/img/campaign.avif" fetchpriority="high" loading="lazy"
     width="1600" height="900" alt="Campaign hero">

<!-- FIXED: attribute on the img, eager, plus a matching preload because
     this template renders the hero after hydration — preload repairs
     discovery, fetchpriority repairs the band -->
<link rel="preload" as="image" href="/img/campaign.avif"
      type="image/avif" fetchpriority="high">
<img src="/img/campaign.avif" fetchpriority="high" loading="eager"
     width="1600" height="900" alt="Campaign hero" decoding="async">

Why a correct hint can still change nothing

The timeline below contrasts three loads of the same 210 KB hero. Only the first gives the hint anything to do early; in the other two, the request is created so late that its band is a footnote.

Hint honoured versus hint made irrelevant Three horizontal request bars on a shared time axis from 0 to 2.5 seconds. Lane one: hint honoured, image request starts at 120 milliseconds and completes at 700 milliseconds. Lane two: JavaScript-injected image, request starts at 1400 milliseconds regardless of the hint. Lane three: loading lazy suppresses the request until 1800 milliseconds. The LCP good threshold at 2.5 seconds is marked. 0 500ms 1.0s 1.5s 2.0s 2.5s LCP "good" ceiling 2.5s Static img + hint honoured fetch 120–700ms paint ≈ 0.9s JS-injected img (late discovery) no request exists — nothing to prioritize fetch 1.4–2.0s paint ≈ 2.2s loading=lazy overrides hint request withheld until viewport-proximity check fetch 1.8–2.3s early fetch, hint effective late discovery suppressed by lazy loading

Deterministic fix protocol

Work top to bottom; each step either clears a cause or confirms it.

  • [ ] 1. Verify the attribute in the delivered HTML. Use view-source (not the Elements panel, which shows the post-JavaScript DOM) on the production URL. Search for fetchpriority. If it is missing but present in your repository, an intermediary — CDN HTML optimizer, edge minifier, consent-platform rewriter — is stripping it; disable that feature for the hero markup or allowlist the attribute.
  • [ ] 2. Confirm it sits on the <img> element itself. Not on <picture>, not on a wrapping anchor, not on a <div> carrying the background style. If the hero is a CSS background, there is no element to hint — replace it with an <img> under the styled container, or add a preload as in step 5.
  • [ ] 3. Remove loading="lazy" and any lazy-load library class from the LCP element. Check for data-src/data-srcset swap patterns too: if src points at a 1 px placeholder in the raw HTML, the real request is script-created and late by construction. The LCP image must ship its real URL in src.
  • [ ] 4. Check the Priority column against the start time. In the Network panel (Priority column on, Big request rows enabled), the image should read High from its first appearance — an initial Low that flips to High later means the hint is absent and you are watching the layout-time viewport boost instead. Then read the waterfall timing bars: a start beyond ~300 ms on a warm connection means discovery, not priority, is your remaining problem.
  • [ ] 5. Add a matching preload for late-discovered heroes. <link rel="preload" as="image" fetchpriority="high"> in the static <head>, with imagesrcset/imagesizes mirroring the element exactly if it is responsive. Re-check the waterfall for a single request row — two rows for the hero URL means the preload and element URLs diverge.
  • [ ] 6. Fix any script-side casing. Grep the codebase for \.fetchpriority\s*= — assignments through the lowercase property name are inert. Use setAttribute('fetchpriority', 'high') or the fetchPriority property, set before src.
  • [ ] 7. Re-measure. Hard-reload under Fast 4G throttling three times; record the image’s start time, priority, and the LCP marker in the Performance panel. Then confirm at the 75th percentile in field data before closing the ticket.

Before/after metrics

Measured on a product-detail template (210 KB AVIF hero, Fast 4G emulation, cold cache, median of 9 runs). “Before” is the broken state from the reproduction — hint present in source, stripped by the CDN optimizer, plus loading="lazy" from the CMS plugin.

Metric Broken hint After protocol Delta
Hero request start 1,640 ms 130 ms −1,510 ms
Priority at request creation Low High promoted at discovery
Hero fetch duration 610 ms 560 ms −50 ms
LCP (lab, p50) 2.9 s 1.4 s −52%
LCP (field, p75, mobile) 3.4 s 2.1 s −38%
Requests for hero URL 2 (placeholder swap) 1 −1

Note where the improvement comes from: almost the entire gain is the request start moving earlier. The fetch duration barely changes — which is the whole lesson of this failure class. The attribute’s job is to win scheduling contention, and it can only do that for a request that exists early enough to contend.

FAQ

The Priority column shows High but LCP did not improve — why?

Band position and start time are independent. If the request starts at 1,400 ms because the browser discovered the image late, a High label cannot recover the lost time. Check startTime in the Resource Timing API: a late start means the fix is a preload or moving the image into server-rendered markup, not a priority hint.

My framework sets priority as a prop — is that enough?

Only if the rendered HTML actually contains fetchpriority="high" on the <img> element in the server response. Some component libraries map the prop to a preload link, some to the attribute, and some drop it silently on older versions. Always verify the raw HTML delivered over the network, not the JSX or template source.

Yes. Client-side experimentation tools that re-render the hero region clone or rebuild DOM nodes, and a rebuild that omits the attribute reverts the image to heuristic priority. Diff the element in DevTools against view-source, and if they disagree, audit every script that mutates the hero container.


Related