Using fetchpriority to Optimize Critical Media

The fetchpriority attribute is an explicit bandwidth scheduling signal — it tells the browser’s network thread to move a specific resource to the front (or back) of the fetch queue before the preload scanner has finished building the full priority model. This guide is part of the Lazy Loading, Preloading & Fetch Priorities reference, which covers the full spectrum of browser resource scheduling from loading="lazy" through preload vs. prefetch for video and image assets to the IntersectionObserver API.

Without an explicit hint the browser applies a heuristic priority: images in the top viewport get High, images below the fold get Low, scripts blocking the parser get VeryHigh. fetchpriority overrides that heuristic for the cases the browser gets wrong — typically the LCP hero image or a video poster frame that is discovered late because it lives inside a CSS background-image property or a <picture> element with multiple source candidates.

Correct placement of a single fetchpriority="high" on the LCP element routinely reduces Largest Contentful Paint by 15–40% in field data, with the largest gains on HTTP/2 origins where the browser must serialize bandwidth across tens of concurrent sub-resource requests.

Concept & Architecture: How the Browser Assigns Fetch Priority

The browser maintains an internal priority queue per network process (not per tab). Each queued resource is assigned one of five priority bands — VeryHigh, High, Medium, Low, VeryLow — based on resource type and position in the parse tree. The preload scanner runs ahead of the main HTML parser and injects resources into this queue before the DOM is built.

fetchpriority maps to these internal bands as follows:

Attribute value Effective internal priority Typical use
high Raises one band (e.g. MediumHigh, or LowHigh) LCP hero image, above-the-fold video poster
auto (default) No change — browser heuristic applies Most images, most scripts
low Lowers one band Below-fold decorative images, prefetch candidates

The key architectural detail is that fetchpriority="high" does not guarantee the request starts immediately — it only influences ordering within the queue. Bandwidth is still subject to HTTP/2 stream multiplexing limits and connection-level throttling. This is why pairing fetchpriority="high" with <link rel="preload"> — which injects the resource into the preload scanner queue before DOM parsing begins — is more powerful than the attribute alone.

The diagram below shows where fetchpriority sits in the resource-scheduling pipeline:

fetchpriority in the Browser Resource Scheduling Pipeline Diagram showing HTML parser and preload scanner feeding into the priority queue, then the network thread, then the render pipeline. The fetchpriority attribute intercepts between the preload scanner and the priority queue. HTML Parser builds DOM Preload Scanner runs in parallel Priority Queue VeryHigh → VeryLow fetchpriority= "high" / "low" / "auto" Network Thread HTTP/2 streams Render LCP paint overrides heuristic

From attribute to wire format

The five priority bands are an internal abstraction; they have to be projected onto whatever the transport actually understands, and the projection is lossy in ways that explain several otherwise baffling measurements.

Inside Chromium the HTML attribute is parsed into a FetchPriorityHint enum on the resource request, combined with the resource type heuristic to produce a ResourceLoadPriority (kVeryLow, kLow, kMedium, kHigh, kVeryHigh), and finally mapped to a net::RequestPriority (IDLE, LOWEST, LOW, MEDIUM, HIGHEST). That last enum is the one the socket pool sees. On HTTP/1.1 it decides nothing more than the order in which requests are handed to the six available sockets per origin — there is no in-flight reordering, so a HIGHEST request queued behind a LOW one that has already started must wait for the connection to free.

On HTTP/2 the browser historically expressed priority through the stream dependency tree of RFC 7540. Almost no server implemented it faithfully, and it was formally deprecated. Chromium, Firefox and Safari now use RFC 9218 Extensible Priorities: a priority request header (and, for mid-stream changes, a PRIORITY_UPDATE frame) carrying two parameters — u, an urgency from 0 (most urgent) to 7, defaulting to 3; and i, a boolean “incremental” flag meaning the response is useful before it is complete.

The mapping matters in practice. An image request that carries fetchpriority="high" is sent with roughly priority: u=2, i — urgent, and incremental because a progressively-encoded image paints as it arrives. A below-fold image sits at u=5 or u=6. Because the header is on the wire, a CDN that honours RFC 9218 will schedule bytes accordingly even under contention; a CDN that ignores it will round-robin every open stream regardless of what the attribute said. That is the single biggest reason the same page shows a 300 ms LCP improvement on one CDN and none on another.

Warning: fetchpriority changes scheduling, never bandwidth. On a saturated link the sum of all transfers is fixed; raising one resource necessarily lowers another. The attribute is a statement about which resource should finish first, not a request for more capacity.

Benchmark Data: Priority Hint Impact on LCP

The numbers below are derived from controlled WebPageTest runs on a 4G-throttled connection (20 Mbps down, 20 ms RTT) loading a page with a 180 KB AVIF hero image and 12 additional sub-resources.

Configuration LCP (ms) TTFB for hero image (ms) Priority column in DevTools
No hint, image in <img> above fold 1 840 320 High (heuristic)
fetchpriority="high" on <img> 1 540 210 High (explicit)
<link rel="preload" fetchpriority="high"> 1 280 95 VeryHigh
CSS background-image, no hint 2 610 890 Low (discovered late)
CSS background-image + preload + fetchpriority="high" 1 310 110 VeryHigh

Key finding: The preload + fetchpriority="high" combination consistently outperforms the attribute alone because it moves resource discovery ahead of DOM parsing, eliminating the scanner-discovery latency entirely.

Plotting the two columns together makes the mechanism visible: LCP tracks the hero image’s time-to-first-byte almost linearly, because on this test page nothing else is competing for the paint. The CSS background-image row is the outlier in both series — an 890 ms TTFB is not slow bytes, it is 700 ms of the request simply not existing yet while the browser downloads and parses the stylesheet that mentions it.

Measured LCP and hero-image TTFB by priority configuration A grouped vertical bar chart with five configurations. For each, one bar shows LCP in milliseconds and a second shows the hero image time to first byte. Values are 1840 and 320 for no hint, 1540 and 210 for fetchpriority high on the image, 1280 and 95 for preload plus fetchpriority high, 2610 and 890 for a CSS background image with no hint, and 1310 and 110 for a CSS background image with preload and fetchpriority high. 0 1000 2000 milliseconds 1840 320 1540 210 1280 95 2610 890 1310 110 <img> no hint <img> fp=high preload + fp=high CSS bg no hint CSS bg + preload + fp=high LCP (ms) hero TTFB (ms) best worst

Step-by-Step Implementation

Step 1 — Identify the LCP Element

Before adding any hints, confirm which element Lighthouse or Chrome’s Performance panel identifies as the LCP candidate. The LCP element is almost always:

  • The largest <img> or <picture> in the viewport
  • A <video> poster frame
  • A CSS background-image on a hero section element

Run a Lighthouse audit or check PerformancePaintTiming in DevTools → Performance → Timings to find the LCP node. Only that node (or its preload link) should receive fetchpriority="high".

Step 2 — Apply the Attribute to an <img> or <picture>

<!-- Hero image: explicit dimensions prevent CLS; fetchpriority="high" moves it
     to the front of the HTTP/2 stream queue before the preload scanner finishes. -->
<img
  src="/hero.avif"
  srcset="/hero-400.avif 400w, /hero-800.avif 800w, /hero-1600.avif 1600w"
  sizes="(max-width: 800px) 100vw, 800px"
  fetchpriority="high"
  loading="eager"
  decoding="async"
  width="1600"
  height="900"
  alt="Hero product shot"
>

Warning: Do not set loading="lazy" on the same element — the two attributes conflict. fetchpriority="high" signals urgency; loading="lazy" defers fetch until the element is near the viewport. The browser resolves this inconsistency by defaulting to eager loading, but the combination is semantically incorrect and wastes the hint.

The preload scanner cannot see images referenced from CSS. For a hero section that uses background-image, inject a preload link in <head> with a matching media query:

<!-- Preload the desktop hero. The media query must match the breakpoint at which
     the CSS background-image rule applies; mismatched media queries preload a
     resource that may never be rendered. -->
<link
  rel="preload"
  as="image"
  href="/hero-1600.avif"
  imagesrcset="/hero-400.avif 400w, /hero-800.avif 800w, /hero-1600.avif 1600w"
  imagesizes="(max-width: 800px) 100vw, 800px"
  fetchpriority="high"
  crossorigin="anonymous"
>

crossorigin="anonymous" is required when the image is served from a CDN origin — omitting it causes the browser to open a second connection for the same resource (CORS vs non-CORS cache partitioning), effectively doubling the fetch cost.

Step 4 — Apply fetchpriority="low" to Below-Fold Images

Explicitly lowering priority for images outside the viewport frees bandwidth for the LCP element — this is the counterpart to step 2 and equally important:

<!-- Below-fold gallery: explicitly depress priority so the browser does not
     compete with the LCP hero during the critical rendering window. -->
<img
  src="/gallery-1.avif"
  fetchpriority="low"
  loading="lazy"
  width="800"
  height="600"
  alt="Gallery image 1"
>

Step 5 — Use priority in the Fetch API for Programmatic Requests

For dynamic media loaded via JavaScript — for example, a video poster fetched and set as a <canvas> background — pass the priority option to fetch():

// priority: 'high' is part of the Fetch Priority API (Chromium 101+).
// Safari and Firefox honor the standard HTTP/2 prioritization signal instead;
// they do not throw on the unknown option, so this is safe cross-browser.
async function loadCriticalPoster(url) {
  try {
    const response = await fetch(url, {
      priority: 'high',       // Chromium: elevates stream weight in HTTP/2
      credentials: 'same-origin'
    });
    if (!response.ok) throw new Error(`HTTP ${response.status}`);
    return URL.createObjectURL(await response.blob());
  } catch (err) {
    console.warn('[media] Priority poster fetch failed:', err.message);
    return url; // Fall back to standard img src
  }
}

Step 6 — Feature-Detect Before Scripted Injection

When dynamically marking LCP candidates (e.g. in a CMS that does not control <head>), guard the attribute write:

// IDL attribute name is camelCase; the HTML attribute name uses lowercase.
// Check the prototype, not a live element, to avoid false positives
// on browsers that return undefined for unknown attributes.
const supportsFetchPriority = 'fetchPriority' in HTMLImageElement.prototype;

if (supportsFetchPriority) {
  document.querySelectorAll('[data-lcp-candidate]').forEach(el => {
    el.fetchPriority = 'high'; // IDL setter — preferred over setAttribute()
  });
}

Step 7 — Reconcile the Attribute With Framework Image Components

Component-based image abstractions emit fetchpriority for you, and they do it from a different vocabulary. Writing the raw attribute alongside a component prop is how sites end up with two competing signals on one element, or with fetchpriority="high" on four images because a “featured” flag was set in the CMS.

// Next.js: the `priority` prop is the framework's name for the pair
// fetchpriority="high" + loading="eager". Do NOT also pass fetchPriority.
import Image from 'next/image';

export function Hero({ src }) {
  return (
    <Image
      src={src}
      alt="Hero product shot"
      width={1600}
      height={900}
      priority          // emits fetchpriority="high" AND a <link rel=preload>
      sizes="(max-width: 800px) 100vw, 800px"
    />
  );
}

// Astro: <Image /> defaults to loading="lazy"; opt the LCP image out
// explicitly, then add the priority hint as a raw attribute.
// <Image src={hero} alt="Hero" loading="eager" fetchpriority="high" />

Exactly one component per route may carry priority. In Next.js the prop additionally injects a preload <link>, so a template that renders three “priority” cards ships three High-band preloads and reproduces the contention failure described below. The loader-level details of that pipeline are covered in Next.js Image component optimization, and the Astro equivalent in Astro Image and Picture components.

Mapping Elements to Priority Bands

The single most useful mental model is a table of “what band does this element start in, and what does the attribute do to it”. Two facts fall out of it that surprise most engineers. First, fetchpriority="high" on an image the browser already classified as High — a large <img> at the top of the document — moves it from High to VeryHigh, ahead of render-blocking CSS. That is occasionally what you want and frequently a regression, because a page cannot paint the LCP element before its stylesheet arrives anyway.

Second, the attribute is not a universal dial. On an element whose fetch has been deferred by loading="lazy", there is no queued request for the hint to reorder; the value is retained and applied at the moment the intersection logic finally schedules the fetch, which is why lazy-loaded images with fetchpriority="high" show no measurable change. And on a CSS background-image there is no element attribute at all — the only lever is the preload <link>.

Element context versus fetchpriority: resulting priority band A five row by three column matrix. Rows list an image in the initial viewport, an image below the fold, a lazy loaded image, a CSS background image, and a preload link. Columns give the resulting priority band with no hint, with fetchpriority high, and with fetchpriority low. element context default heuristic + fetchpriority="high" + fetchpriority="low" <img> in initial viewport High VeryHigh — ahead of CSS Low — wrecks LCP <img> below the fold Low High — steals bandwidth VeryLow — correct <img loading="lazy"> VeryLow until in range no effect until fetched no effect CSS background-image Low, found after CSSOM n/a — no element n/a — no element <link rel=preload as=image> High VeryHigh — the LCP recipe Medium Bands are Chromium ResourceLoadPriority values as reported in the DevTools Priority column.

Priority Contention: A Worked Bandwidth Example

Consider the concrete case that produces the “I added the hint and LCP got worse” bug report. The page is served over HTTP/2 from one origin on a 20 Mbps link (2.5 MB/s). Four resources are in flight at the start of the load: a 60 KB stylesheet, two 30 KB WOFF2 fonts, and the 180 KB AVIF hero.

With the default heuristic the stylesheet is VeryHigh, the fonts are High, the hero is High. The stylesheet finishes first at roughly 24 ms of transfer, after which the remaining 240 KB shares the link three ways; the hero completes at about 120 ms of transfer time.

Now put fetchpriority="high" on the hero and on two below-fold gallery images, as a careless CMS rule might. Five resources now sit at High or above, totalling 420 KB. The scheduler round-robins between them, so the hero receives roughly one fifth of 2.5 MB/s instead of one third. Its 180 KB now takes about 360 ms of transfer, the stylesheet is delayed behind a fuller queue, and first paint — which requires the CSS — slips with it. Every individual request was marked urgent, so none of them was.

The arithmetic generalises: the transfer time of the LCP asset is approximately size ÷ (link_rate ÷ N), where N is the number of concurrently in-flight requests in the same or a higher band. Reducing N is worth far more than raising the band of one member. That is why the discipline of exactly one fetchpriority="high" per page, paired with explicit fetchpriority="low" on everything decorative, outperforms any amount of tuning on the hero alone.

Parameter Reference

Attribute / option Allowed values Notes
fetchpriority (HTML) high, low, auto Valid on <img>, <link>, <script>, <iframe>. Default is auto.
priority (Fetch API) 'high', 'low', 'auto' Option to fetch(). Chromium 101+; ignored (not thrown) in Safari/Firefox.
rel="preload" as="image" Must accompany imagesrcset/imagesizes when the image uses srcset. Without as, the browser fetches the resource twice.
crossorigin anonymous, use-credentials Required when the image origin differs from the page origin and the image is already fetched with CORS elsewhere on the page.
loading eager, lazy Do not combine loading="lazy" with fetchpriority="high" — semantically contradictory.
decoding async, sync, auto async releases the main thread from image decode; combine with fetchpriority="high" for faster LCP paint.
fetchpriority on <iframe> high, low, auto Applies to the frame’s own document request, not to sub-resources inside it. Useful for a low on third-party embeds.
fetchpriority on <script> high, low, auto low on an async analytics script is one of the cheapest LCP wins available; it does not delay execution once fetched.
imagesrcset / imagesizes srcset / sizes syntax Must be byte-identical to the element’s values, otherwise the hint and the element pick different candidates.
priority header (u, i) u=0..7, i RFC 9218 wire representation of the band. Visible in a curl --http2 -v trace or a QUIC keylog capture.
document.prerendering true / false In a prerendered document all fetches are already complete; do not re-apply priority hints on prerenderingchange.

Browser Compatibility

Feature Chrome Edge Firefox Safari 16 Safari 14
fetchpriority on <img> 101+ 101+ 132+ 17.2+ No
fetchpriority on <link rel="preload"> 101+ 101+ 132+ 17.2+ No
priority in fetch() 101+ 101+ No No No
imagesrcset on <link rel="preload"> 73+ 79+ 78+ 13.1+ 13.1+
<link rel="preload"> without fetchpriority 50+ 17+ 85+ 13.1+ 13.1+
fetchpriority on <script> and <iframe> 102+ 102+ 132+ 17.2+ No
RFC 9218 priority request header 110+ 110+ 121+ 17+ No
PRIORITY_UPDATE frame (HTTP/2 and HTTP/3) 110+ 110+ 121+ Partial No
HTMLImageElement.prototype.fetchPriority IDL 102+ 102+ 132+ 17.2+ No

For Safari 14 and Firefox below 132, <link rel="preload"> without fetchpriority still provides the majority of the benefit by moving image discovery ahead of DOM parsing. The priority band elevation is simply absent — the browser’s own heuristic applies.

Tradeoffs & Edge Cases

Tradeoff: Setting fetchpriority="high" on more than one image starves CSS and fonts. The browser can only service so many High-priority requests concurrently. If three images all carry fetchpriority="high", stylesheets queued at the same priority band may be delayed, introducing a flash of unstyled content. Apply fetchpriority="high" to a single LCP element per page load — the one Lighthouse identifies.

Tradeoff: fetchpriority is invisible to CDN prefetch logic. Content delivery networks that implement server-push or Early Hints (HTTP 103) use the Link response header, not the HTML attribute. If you configure Cache-Control headers for image and video assets at the CDN edge to push the hero image, the fetchpriority attribute in HTML is redundant but harmless.

Warning: Mismatched media on <link rel="preload"> causes duplicate fetches. If the media attribute of the preload link does not match the CSS breakpoint at which the image is actually used, the browser downloads both the preloaded resource and the image triggered by the CSS rule — wasting bandwidth and potentially inflating LCP.

Tradeoff: fetchpriority="low" applied to the LCP image is a catastrophic misconfiguration. CMS or template systems that apply fetchpriority="low" to all images for “bandwidth saving” will actively harm LCP. Always audit your CMS output with DevTools before deploying site-wide priority policies.

Edge case: Chromium downgrades fetchpriority="high" images when the connection is throttled. On 2G-equivalent connections, Chromium’s network scheduler applies a throttle that ignores explicit priority for requests that arrive after the first 60 KB of the HTML body. Use <link rel="preload"> in <head> to ensure the request is queued before this threshold is reached.

Edge case: <video> has no fetchpriority attribute. The element itself is not in the list of elements that accept the attribute; only the poster image can be prioritised, and only indirectly through a preload <link as="image"> pointing at the poster URL. If the video is the LCP candidate, the poster frame is what LCP actually measures, so prioritising the poster is the correct and sufficient move. Autoplaying background clips should go the other way entirely — see how to implement lazy loading for WebM backgrounds.

Edge case: priority is fixed at request time on HTTP/1.1. Changing el.fetchPriority after the request has started has no effect on an HTTP/1.1 connection because there is no mechanism to reprioritise an in-flight response. On HTTP/2 and HTTP/3 the browser can emit a PRIORITY_UPDATE frame, but only if the server advertises support. Treat runtime priority mutation as advisory and set the value in the initial markup wherever possible.

Warning: a srcset mismatch silently doubles the download. When a preload carries imagesrcset, the browser runs candidate selection twice — once for the hint, once for the element — and the two runs must agree. A sizes value of 100vw on the link and (max-width: 800px) 100vw, 800px on the image will diverge on a 1200 px viewport and fetch two different files, both at elevated priority. Generate both strings from the same source of truth in your template.

Tradeoff: explicit fetchpriority="low" versus loading="lazy". They solve overlapping problems differently. loading="lazy" removes the request entirely until the element approaches the viewport, saving bytes; fetchpriority="low" keeps the request but yields bandwidth, so the image is still available immediately on scroll. For a carousel whose second slide appears within a second of load, low is the better choice — lazy produces a visible blank frame.

Debugging & Validation

Confirm Priority in Chrome DevTools Network Panel

Open DevTools → Network, right-click the column header, and enable the Priority column. After a page load, locate your LCP image and verify it shows High (explicit) rather than High (heuristic). The distinction matters: explicit means your fetchpriority attribute was applied; heuristic means the browser upgraded it independently (and would still do so if you removed the attribute).

# Fetch the page headers to confirm the preload Link header is present at the CDN edge.
# Replace the URL with your actual page URL.
curl -sI https://example.com/ | grep -i "link:"

Measure Priority Scheduling in the Performance Panel

  1. Open DevTools → Performance → Start profiling → Reload.
  2. In the Network track, locate the LCP image bar.
  3. Hover to confirm the “Queued at” timestamp is within the first 200 ms of navigation — this indicates the preload scanner discovered it early.
  4. Compare startTime vs responseEnd in PerformanceResourceTiming:
// Log timing data for all image resources to identify priority scheduling gaps.
performance.getEntriesByType('resource')
  .filter(e => e.initiatorType === 'img' || e.name.match(/\.(avif|webp|jpg|png)/))
  .forEach(e => {
    console.log(e.name, {
      queuedAt: Math.round(e.startTime),         // ms from navigation start
      ttfb: Math.round(e.responseStart - e.requestStart), // server response latency
      total: Math.round(e.duration)
    });
  });

Lighthouse Audit

Run lighthouse https://example.com --only-audits=largest-contentful-paint,uses-rel-preload,prioritize-lcp-image --output json and inspect the prioritize-lcp-image audit. A passing score confirms Chromium detected a fetchpriority="high" hint on the LCP element.

Read the Wire Priority Directly

The DevTools Priority column reports Chromium’s internal band. To see what the server was told, capture the request headers on an HTTP/2 or HTTP/3 connection:

# --http2 forces h2; -v prints the request pseudo-headers and the
# RFC 9218 priority header if the client emits one.
# Look for a line like:  priority: u=2, i
curl --http2 -v -o /dev/null \
  -H 'accept: image/avif,image/webp,*/*' \
  https://your-cdn.com/hero.avif 2>&1 | grep -i '^> priority'

An absent header means the client is defaulting (u=3) and the origin is scheduling by arrival order alone. If the header is present but the CDN still delivers bytes round-robin, the edge does not implement extensible priorities — the attribute will show a DevTools change with no LCP change, which is exactly the discrepancy the benchmark table warns about.

Assert the Policy in CI

Priority regressions are introduced by templates, not by hand-written pages, so the durable fix is an assertion in the build rather than a manual audit.

// priority-budget.test.js — fail the build if a template emits more than one
// high-priority image hint. Run against rendered HTML, not source templates.
import { JSDOM } from 'jsdom';

export function assertPriorityBudget(html) {
  const { document } = new JSDOM(html).window;
  const highImages = document.querySelectorAll(
    'img[fetchpriority="high"], link[rel="preload"][as="image"][fetchpriority="high"]'
  );
  if (highImages.length > 1) {
    throw new Error(
      `Priority budget exceeded: ${highImages.length} high-priority image hints`
    );
  }
  // A lazy element carrying a high hint is always a template bug.
  const contradictions = document.querySelectorAll(
    'img[loading="lazy"][fetchpriority="high"]'
  );
  if (contradictions.length) {
    throw new Error('loading="lazy" combined with fetchpriority="high"');
  }
}

For more complex priority conflicts — multiple competing hints, CDN push interference, or priority downgrade under throttling — see Debugging fetchpriority conflicts in Chrome DevTools for a systematic DevTools-first diagnosis workflow.

Frequently Asked Questions

Does fetchpriority="high" make the download faster?

No. It changes the order in which the browser hands requests to the transport and the urgency it advertises to the server. On an uncontended connection — a fast desktop link loading a light page — the ordering is irrelevant and the attribute measures as noise. Its value appears exactly when the link is saturated, which is why field data from mobile users shows far larger gains than a local test.

Can I use fetchpriority on a <video> element?

The attribute is not defined on <video>. Prioritise the poster image instead with <link rel="preload" as="image">, since the poster is what Largest Contentful Paint measures for a video hero. For the media file itself, choose the format and codec carefully instead — understanding video codecs: VP9 vs H.265 vs AV1 has the size comparisons that matter far more than scheduling.

Why does the DevTools Priority column show High even without the attribute?

Chromium’s heuristic already promotes images that are in the initial viewport and above a size threshold once layout has run. When the heuristic gets it right, adding the attribute is a no-op — and that is a valid outcome. The attribute earns its place on elements the heuristic gets wrong: late-discovered images, images inside <picture> with several candidates, and anything referenced from CSS.

Should every below-fold image get fetchpriority="low"?

Every below-fold image that is not already loading="lazy", yes. Once an image is lazy-loaded the browser has already deferred the request, and the extra attribute changes nothing. The combination worth auditing for is a below-fold image that is neither lazy nor low — that is the one competing with your hero.

Does the attribute survive a bfcache restore or a prerender?

Both cases bypass it. A bfcache restore resurrects the fully-loaded document with no new requests, and a prerendered document completed its fetches before the click. Check document.prerendering before re-running any priority logic on activation, otherwise you queue duplicate requests for assets that are already decoded.

How do I prove the attribute, not something else, moved LCP?

Run the same URL twice through WebPageTest with only the attribute differing, and compare the hero image’s responseStart rather than LCP itself. LCP is sensitive to layout, font loading and main-thread work; responseStart isolates the scheduling change. A shift in responseStart with no shift in LCP means the bottleneck is downstream of the network.