Debugging fetchpriority Conflicts in Chrome DevTools

When competing resource hints force Chrome’s network scheduler to negotiate multiple Highest-priority requests simultaneously, critical media assets stall — inflating LCP and masking the gains that fetchpriority on the LCP image was supposed to deliver. This page is a deterministic, DevTools-first workflow for isolating, diagnosing, and resolving those conflicts. It builds directly on the Lazy Loading, Preloading & Fetch Priorities reference.

Prerequisite checklist

Before opening DevTools, confirm the following are already in place:

Exact solution: Network panel priority audit

The Network panel’s Priority column is the single authoritative view of what the scheduler actually assigned to each request. This is distinct from what you declared in HTML — Chrome can and does override hints.

Step 1 — Open DevTools:
  F12 (Windows/Linux) or Cmd + Option + I (macOS)
  Navigate to the Network tab.

Step 2 — Enable the Priority column:
  Right-click any column header → tick "Priority".
  Sort the column descending (Highest → Low).

Step 3 — Simulate field conditions:
  Throttling dropdown → "Fast 3G"
  Tick "Disable cache" (bypasses both HTTP cache and service worker).

Step 4 — Reload:
  Cmd/Ctrl + Shift + R (hard reload, skips disk cache).

Step 5 — Identify stalled high-priority requests:
  Filter by "Img" type. Look for any row where:
    Priority = Highest   AND   Stalled column > 500 ms
  These are priority inversions — assets the scheduler wanted to
  fetch first but could not due to connection-pool contention or
  a conflicting directive on the same resource.

The diagram below shows a typical priority-inversion waterfall before and after removing a redundant preload:

fetchpriority waterfall — before and after removing redundant preload Two side-by-side network waterfalls. On the left, hero.webp shows a long Stalled bar before the actual download begins. On the right, after removing the duplicate preload directive, hero.webp starts downloading immediately with no stall. Before — duplicate preload hero.webp Highest Stalled 620ms preload hero.webp Highest Queue hero-alt.webp High LCP ~2.8s After — single fetchpriority="high" hero.webp Highest DL 380ms hero-alt.webp High LCP ~1.9s Stalled / Queueing Downloading Connection queue Removing the duplicate <link rel="preload"> eliminated the 620 ms stall and reduced LCP by ~0.9 s. Simulated on Fast 3G throttling (40 Mbit/s down, 20 ms RTT) in Chrome 124.

Correct HTML pattern — single directive, no duplication

<!--
  fetchpriority="high"  — tells the preload scanner this is the LCP image.
                          Apply to AT MOST ONE image per page; multiple "high"
                          declarations trigger starvation in the H2 connection pool.
  loading="eager"       — required when fetchpriority="high" is set; "lazy" silently
                          overrides the priority hint and defers the fetch.
  decoding="async"      — offloads JPEG/WebP/AVIF decompression off the main thread
                          so it does not block INP.
  width/height          — prevent layout shift (CLS); always include both.
-->
<img
  src="/hero.webp"
  fetchpriority="high"
  loading="eager"
  decoding="async"
  alt="Primary hero visual"
  width="1200"
  height="630"
/>

Warning: Do not also add <link rel="preload" as="image" href="/hero.webp"> when fetchpriority="high" is already set on the <img>. The preload link creates a second Highest-priority scheduler entry for the same byte range. Chrome issues both fetches, collides on the same H2 stream slots, and the LCP image ends up stalled behind its own preload.

Verification steps

1. Console DOM audit

Run this in the DevTools Console to surface all competing high-priority hints declared in the live DOM:

// Maps every declarative fetchpriority=high image AND every preload-as-image
// link to a structured table. Look for duplicate src/href values — they indicate
// a redundant hint that will cause scheduler contention.
const hints = Array.from(
  document.querySelectorAll(
    'img[fetchpriority="high"], link[rel="preload"][as="image"]'
  )
).map(el => ({
  tag:      el.tagName,
  src:      el.src || el.href,            // canonical URL after resolution
  priority: el.getAttribute('fetchpriority') || '(none)',
  loading:  el.getAttribute('loading')   || 'auto',
  // Note: dynamically injected hints (e.g. from framework hydration) will
  // appear here IF they were already added at query time. For SSR hydration
  // races, attach a MutationObserver before DOMContentLoaded fires.
}));

console.table(hints);
// Red flag: two rows with the same src/href value.

2. Automated CI check with Puppeteer

For regression testing in CI, capture stalled image timings headlessly before merging:

// Puppeteer: report image requests where sendStart > 400 ms — the threshold
// above which scheduler stalls materially inflate LCP on mobile connections.
// npm install puppeteer
const puppeteer = require('puppeteer');

(async () => {
  const browser = await puppeteer.launch({
    args: ['--no-sandbox'],        // required in most CI containers
  });
  const page = await browser.newPage();

  // Emulate a mid-range Android device on Fast 3G
  await page.emulate(puppeteer.KnownDevices['Moto G Power']);
  await page.emulateNetworkConditions(
    puppeteer.networkConditions['Fast 3G']  // 40 Mbps down, 20 ms RTT
  );

  const stalled = [];
  page.on('response', async response => {
    const url  = response.url();
    const timing = response.timing();
    // timing.sendStart = ms from request start to first byte sent;
    // a high value on an image request signals scheduler stall.
    if (timing && timing.sendStart > 400 && /\.(avif|webp|jpe?g|png)$/.test(url)) {
      stalled.push({ url, stallMs: timing.sendStart.toFixed(0) });
    }
  });

  await page.goto('https://your-site.com', { waitUntil: 'networkidle0' });

  if (stalled.length) {
    console.error('Priority-stalled image requests detected:');
    console.table(stalled);
    process.exitCode = 1;   // fail the CI step
  } else {
    console.log('No stalled high-priority image requests.');
  }
  await browser.close();
})();

Warning: The Chrome DevTools Protocol does not expose a synchronous getNetworkRequests() call. You must attach event listeners before navigation — placing the page.on('response', …) handler after page.goto() will miss early requests.

3. CLI validation commands

# Generate a Lighthouse performance JSON baseline for LCP comparison
# --throttling-method=devtools matches the Fast 3G preset used in manual testing
lighthouse https://your-site.com \
  --only-categories=performance \
  --output=json \
  --throttling-method=devtools \
  --output-path=./lcp-baseline.json

# Extract every fetchpriority declaration from the rendered (post-JS) DOM.
# google-chrome --dump-dom runs the page through V8 before printing the DOM,
# so framework-injected hints are included in the output.
google-chrome \
  --headless \
  --disable-gpu \
  --dump-dom \
  https://your-site.com \
  | grep -i 'fetchpriority\|rel="preload"' \
  | sort | uniq -c | sort -rn
# Expected: exactly ONE fetchpriority="high" line, no duplicate preload hrefs.

# Inspect CDN response headers for cache or priority stripping.
# A CDN that strips Vary: Accept may serve the wrong image format;
# one that strips Cache-Control directives may break immutable caching.
# See /core-media-fundamentals-next-gen-formats/cache-control-headers-for-image-and-video-assets/
curl -sI https://your-site.com/hero.webp \
  | grep -i 'cache-control\|vary\|content-type\|priority'

Common mistakes & fixes

1. Pairing fetchpriority="high" with loading="lazy"

<!-- WRONG: the browser ignores fetchpriority on lazy-loaded images.
     The image will be deferred regardless of the priority declaration. -->
<img src="/hero.webp" fetchpriority="high" loading="lazy" alt="Hero" />

<!-- CORRECT: loading="eager" is required for the priority hint to take effect. -->
<img src="/hero.webp" fetchpriority="high" loading="eager" alt="Hero" />

2. Multiple fetchpriority="high" images on the same page

Tradeoff: Promoting two or more images to Highest priority does not serve them faster — it collapses priority differentiation and forces the H2 multiplexer to treat all of them equally, which effectively stalls each one while waiting for the others.

<!-- WRONG: two Highest-priority requests compete for the same H2 connection slots. -->
<img src="/hero.webp"    fetchpriority="high" loading="eager" alt="Hero" />
<img src="/feature.webp" fetchpriority="high" loading="eager" alt="Feature" />

<!-- CORRECT: one high, one auto (default). The scheduler will fetch the
     high-priority image first and the second in normal order. -->
<img src="/hero.webp"    fetchpriority="high" loading="eager" alt="Hero" />
<img src="/feature.webp"                      loading="eager" alt="Feature" />
<!-- WRONG: duplicate Highest-priority entries for the same byte range. -->
<link rel="preload" as="image" href="/hero.webp" />
<img src="/hero.webp" fetchpriority="high" loading="eager" alt="Hero" />

<!-- CORRECT: remove the preload link; fetchpriority communicates priority
     to the preload scanner without an extra DOM node. -->
<img src="/hero.webp" fetchpriority="high" loading="eager" alt="Hero" />

4. Framework hydration overriding native hints

Many meta-frameworks (Next.js, Nuxt, Astro) auto-inject <link rel="preload"> for above-the-fold assets during SSR. If fetchpriority="high" is already set on the <img>, this creates the duplicate-hint anti-pattern at runtime.

// Next.js — disable automatic preload injection for non-LCP images.
// The priority prop sets fetchpriority="high" AND adds a preload link.
// Use it ONLY for the single LCP image; omit it for all others.

// WRONG: priority on two images
<Image src="/hero.webp"    priority={true} alt="Hero" />
<Image src="/feature.webp" priority={true} alt="Feature" />  // duplicate Highest

// CORRECT: priority only on the LCP image
<Image src="/hero.webp"    priority={true}  alt="Hero" />
<Image src="/feature.webp" priority={false} alt="Feature" />

5. CDN stripping the Priority HTTP request header

Browsers that support Priority Hints send a Priority: u=0, i (RFC 9218) request header alongside fetchpriority="high". Some CDN edge configurations strip unknown request headers before forwarding to origin, discarding the hint. The fix is CDN-specific: on Cloudflare, ensure no Transform Rules remove the Priority header; on AWS CloudFront, add Priority to the CachePolicy allowed headers list. Use curl -v and inspect the forwarded request headers in your origin access log to confirm the header arrives.

Expected metric deltas after fix

Metric Target improvement Validation method
LCP load time –15 % to –35 % Lighthouse / WebPageTest
Stalled Highest-priority requests 0 DevTools Network → Priority column
Main-thread blocking (image decode) ~120 ms reduction DevTools Performance → Main thread
CLS score No regression (±0.01) Lighthouse / RUM dashboard