The fetchpriority Attribute & Priority Hints

Browsers guess. Every request a page issues is assigned a scheduler band by heuristic — resource type, position in the markup, whether layout has confirmed the element is in the viewport — and the heuristic is wrong often enough to matter. A hero image idles in the low band while an analytics script occupies a connection slot; a JSON payload that gates rendering queues behind six decorative thumbnails. The Priority Hints specification exists to correct exactly these misjudgements: it defines the fetchpriority content attribute for <img>, <link>, <script>, and <iframe>, plus a matching priority option on fetch(), letting the author shift a request’s computed priority up or down relative to what the heuristic would have chosen.

The operative word is shift. fetchpriority never pins a request to an absolute band, and it is advisory — a signal the browser weighs, not a command it must obey. Understanding what each value does per resource type, and what the attribute cannot do, is the difference between a hint that shaves 800 ms off your LCP and a sprinkle of attributes that changes nothing. This guide covers the spec surface, the per-type shift semantics, engine support, a step-by-step rollout, verification in DevTools and Lighthouse, and the edge cases where hints silently stop working.


What Priority Hints actually control

The browser’s scheduler places each request into one of five internal bands — Chromium labels them VeryHigh, High, Medium, Low, and VeryLow and surfaces them in DevTools as Highest through Lowest. The full band model is covered in the guide to browser resource priority queues; what matters here is the mechanism the Priority Hints spec adds on top of it.

Each resource type has a default computed priority, and fetchpriority moves the request one step relative to that default:

  • fetchpriority="high" — raise the request above its type default.
  • fetchpriority="low" — lower the request below its type default.
  • fetchpriority="auto" — the explicit default: let the heuristic decide. Identical to omitting the attribute.

Because the shift is relative, the same value produces different absolute outcomes on different elements. high on an async script lifts it from Low to High; high on a synchronous head script does nothing, because parser-blocking scripts already sit at the top of the range available to scripts. Conversely, low on an <iframe> is one of the most powerful demotions available, dropping the frame’s document request from High to Low and taking its entire subresource tree down with it.

The second thing the spec controls is invisible in markup: the priority option on fetch():

// Scheduling rationale: product data gates first render, so it must not
// queue behind image traffic — 'high' keeps it in the same band as
// parser-discovered critical requests.
const product = await fetch('/api/product/4211', { priority: 'high' });

// Scheduling rationale: telemetry has no render dependency; 'low' yields
// its bandwidth share to anything the user is waiting on.
fetch('/api/beacon', { method: 'POST', body: payload, priority: 'low' });

Programmatic fetches default to High in every engine — the browser assumes an explicit fetch() call is application-critical. On data-heavy pages that assumption is frequently backwards, which makes priority: 'low' on telemetry, prefetch warming, and speculative API calls one of the cheapest wins the spec offers.

What hints do not control

Three boundaries are worth internalizing before writing any attributes:

  1. Hints do not affect discovery. A request must exist before it can be reprioritized. An image referenced from a CSS background-image or injected by JavaScript after hydration is discovered late no matter what attribute you intend for it; discovery problems are solved by preload, not by Priority Hints.
  2. Hints do not cross the render-blocking ceiling. The document itself and render-blocking CSS occupy the top band unconditionally. No fetchpriority value places a subresource above them.
  3. Hints are advisory. The spec explicitly permits engines to ignore them. All shipping implementations honour them in the common cases, but the contract is “may influence”, not “must set” — build your loading strategy so it degrades gracefully when the hint is a no-op.

The diagram below shows the shift mechanics across the band ladder for the four most commonly hinted request types.

fetchpriority band shifts by resource type A ladder of five priority bands from Highest to Lowest. Four request types are shown at their default band with arrows indicating where fetchpriority high or low moves them: an out-of-viewport image rises from Low to High, an async script rises from Low to High, a fetch call drops from High to Low, and an iframe drops from High to Low. Render-blocking CSS sits fixed in the Highest band, unreachable by hints. Highest High Medium Low Lowest document + CSS hints cannot reach img (below fold) default: Low img fetchpriority=high script async default: Low script async fetchpriority=high fetch() / iframe default: High fetch() / iframe priority / fp = low promotion via high demotion via low heuristic default band Shifts are relative to the type default — no hint outranks the render-blocking ceiling.

Default priority and shift behaviour by resource type

The table below is the working reference for Chromium’s computed priorities and how each fetchpriority value moves them. Treat the band names as Chromium’s DevTools labels; WebKit and Gecko implement equivalent shifts on their own internal scales.

Request type Default (auto) With fetchpriority="high" With fetchpriority="low"
Render-blocking CSS in <head> Highest Highest (no change) Highest (no change)
Synchronous <script> before content High High (no change) Low
<script async> / <script defer> Low High Low (no change)
<script type="module"> Low High Low (no change)
<img> outside viewport at parse time Low High Low (no change)
<img> confirmed in viewport by layout Medium → High (boosted) High from the start Low, no boost
<link rel="preload" as="image"> Low High Low (no change)
<link rel="preload" as="script"> High High (no change) Low
<link rel="preload" as="fetch"> High High (no change) Low
<iframe> document High High (no change) Low (subtree inherits)
fetch() / XHR High High (no change) Low
<link rel="prefetch"> Lowest Lowest (hint ignored) Lowest

Three patterns fall out of this table. First, high is only meaningful on types whose default is Low — images, async and deferred scripts, image preloads. Second, low is only meaningful on types whose default is High — fetch(), iframes, script and fetch preloads. Third, the two ends of the range are immovable: render-blocking CSS cannot be demoted by a hint, and prefetch cannot be promoted out of the idle band (if a future-navigation resource turns out to be current-navigation critical, the correct fix is switching the hint type, not attaching fetchpriority).

One nuance deserves emphasis because it drives most real-world image wins: without a hint, an image that will end up in the viewport starts life at Low and is boosted only after layout confirms its position — typically hundreds of milliseconds into the load. fetchpriority="high" removes that round trip through layout entirely, granting the high band at discovery time. This early-versus-boosted distinction is invisible in a summary table but obvious in a network waterfall, where the hinted image’s request bar starts alongside the stylesheets instead of after first layout.


Browser support matrix

Feature Chrome / Edge (Chromium) Safari (WebKit) Firefox (Gecko)
fetchpriority on <img> 102 17.2 132
fetchpriority on <link> (preload) 101 17.2 132
fetchpriority on <script> 102 17.2 132
fetchpriority on <iframe> 102 17.2 Not implemented
fetch() priority option 101 17.2 132
fetchPriority IDL property reflection 102 17.2 132
Hint visible in DevTools priority column Yes (Priority column) Partial (priority not surfaced per-request) Yes (Priority column)

Engine differences worth planning around:

Behaviour Chromium WebKit Gecko
Image in-viewport boost without hint Low → boosted after layout Similar two-phase behaviour Images generally fetched eagerly at type default
high on async script Low → High Honoured Honoured; internal weight differs
low on <iframe> Frame + subtree demoted Honoured Ignored (attribute not implemented on iframe)
Effect on HTTP/2 / HTTP/3 stream priority signals Hint feeds urgency in the priority header and stream weights Maps to internal scheduling; weaker protocol coupling Maps to internal scheduling; weaker protocol coupling

The support floor — Safari 17.2 and Firefox 132 — is recent enough that a meaningful share of traffic still ignores the attribute. That is acceptable by design: an ignored fetchpriority leaves the heuristic default in place, so the attribute is safe to ship unconditionally. What you must not do is treat the hint as a functional dependency (for example, relying on priority: 'low' to keep a fetch from contending with your LCP on all browsers — on an engine that ignores it, contention returns).


Implementation, step by step

Step 1 — Baseline before touching anything

Open Chrome DevTools → Network, right-click the column header, and enable Priority. Apply Fast 4G throttling, hard-reload, and screenshot the first twenty rows. You are looking for two mismatches: critical resources sitting at Low or Medium, and non-critical resources sitting at High. Everything that follows targets one of those two lists. If you skip the baseline you cannot prove the hints did anything — verification depends on a before/after diff.

Step 2 — Elevate the LCP-driving image (one element only)

<!-- Scheduling rationale: the hero is the LCP element; high grants the
     top image band at discovery instead of waiting for the post-layout
     in-viewport boost, saving one layout round trip of queue time. -->
<img src="/img/hero-1600.avif"
     srcset="/img/hero-800.avif 800w, /img/hero-1600.avif 1600w"
     sizes="(max-width: 800px) 100vw, 1600px"
     width="1600" height="900"
     alt="Editor timeline with live collaboration cursors"
     fetchpriority="high" decoding="async">

The budget for high on images is effectively one per template. The full diagnosis path for when this hint fails to move LCP — typos, late discovery, lazy-loading conflicts, CDN rewriting — is covered in fetchpriority=high not working on your LCP image.

Step 3 — Demote high-band requests that are not render-critical

<!-- Scheduling rationale: the chat widget frame defaults to High and drags
     its whole subresource tree with it; low pushes the entire subtree
     behind first-party critical requests. -->
<iframe src="https://chat.example-widget.invalid/embed"
        title="Support chat" fetchpriority="low" loading="lazy"></iframe>

<!-- Scheduling rationale: A/B bucketing must run early (so async, not defer)
     but its bytes are small and non-render-critical — low keeps it from
     competing with the hero image for the first connection slots. -->
<script src="/js/experiments.js" async fetchpriority="low"></script>

Demotions are the underused half of the spec. Every request you move out of the high band returns bandwidth to the requests you left in it — often worth more than the single promotion in Step 2.

Step 4 — Hint programmatic fetches

// Scheduling rationale: split data fetches by render dependency — the
// listing JSON blocks meaningful paint (high); recommendations render
// below the fold after idle (low). Both default to High without hints,
// which would make them compete head-to-head.
const listing = fetch('/api/listing?page=1', { priority: 'high' });
const recs    = fetch('/api/recommendations', { priority: 'low' });

const listingData = await (await listing).json(); // render path
renderListing(listingData);

(await recs).json().then(renderRecommendations); // fills in later

Because every fetch() defaults to High, a page issuing eight API calls at boot has eight requests fighting the LCP image. Classifying them explicitly is usually a one-line change per call site.

Step 5 — Combine with preload where discovery is also late

<!-- Scheduling rationale: the poster is referenced from CSS, so the parser
     never sees it — preload fixes discovery, fetchpriority fixes the band.
     Preload as=image defaults to Low, so without the attribute this hint
     would fetch early but slowly. -->
<link rel="preload" as="image" href="/img/poster-1200.webp"
      type="image/webp" fetchpriority="high">

Remember the division of labour: preload moves the start of the request earlier; fetchpriority moves its queue position once started. Diagnose which of the two you are missing before applying both reflexively.

Step 6 — Reflect hints correctly from JavaScript

// Scheduling rationale: elements created after parse never met the preload
// scanner, so the band assignment happens at insertion — set the IDL
// property (camelCase) before assigning src, or the request is created
// with the default priority and the hint arrives too late.
const img = document.createElement('img');
img.fetchPriority = 'high';   // property is fetchPriority; attribute is fetchpriority
img.width = 1600;
img.height = 900;
img.alt = 'Checkout summary';
img.src = '/img/checkout-hero.avif'; // request is created here
document.querySelector('.hero-slot').replaceChildren(img);

The casing split trips up more teams than any other detail: in HTML the attribute is all-lowercase fetchpriority; in the DOM the reflected property is camelCase fetchPriority. Setting img.fetchpriority = 'high' creates an inert expando property and silently changes nothing.


Verification workflow

DevTools Priority column

  1. Network panel → Priority column (right-click the header to enable it; enable Big request rows to see both initial and final priority when they differ). Hard-reload with throttling on.
  2. Confirm each hinted request landed in its intended band: the hero image at High from its first appearance, demoted iframes and fetches at Low.
  3. Hover the Waterfall bar of a demoted request. Expect its light-shaded queueing segment to lengthen — that is the demotion visibly yielding bandwidth. On an uncontended fast connection you may see no change at all; hints only manifest when requests actually compete.
  4. Check the Initiator column for duplicates: a preload whose URL fails to match its consumer produces two rows for the same asset, and the hint on the wrong row is wasted.

Lighthouse

Run a performance audit and open two sections. “Avoid chaining critical requests” (the big request chains view) shows whether your promoted resource still sits at the end of a long dependency chain — a hint cannot shorten a chain, only reorder siblings, so a deep chain here means the fix is architectural, not attribute-level. Second, the “Preload Largest Contentful Paint image” and prioritization diagnostics call out an LCP image that is discoverable but under-prioritized; after a correct rollout this audit should pass.

PerformanceObserver spot-check

// Verification rationale: renderBlockingStatus plus timing exposes whether
// the promoted request actually started early — a high hint on a
// late-discovered resource shows a large startTime despite the band change.
new PerformanceObserver((list) => {
  for (const e of list.getEntries()) {
    if (e.name.includes('hero')) {
      console.table({
        start: Math.round(e.startTime),
        queued: Math.round(e.requestStart - e.startTime),
        duration: Math.round(e.duration),
        transferSize: e.transferSize
      });
    }
  }
}).observe({ type: 'resource', buffered: true });

A successful promotion shows up as a small queued value and a start close to the stylesheet requests. A large start with a small queued value means your problem was discovery, not priority.


Edge cases and gotchas

fetchpriority and loading=lazy point in opposite directions

loading="lazy" withholds the request until the element approaches the viewport; fetchpriority reorders a request that exists. Combined on one element, laziness wins — there is nothing to prioritize until the deferred fetch is finally issued, at which point the hint applies to a request that is already catastrophically late. Never pair loading="lazy" with fetchpriority="high". The pairing with low is coherent, though largely redundant: a lazily-triggered request near the viewport is usually wanted soon, so demoting it can backfire. The mechanics of choosing between deferral and demotion are covered in deprioritizing below-the-fold images.

Preload + fetchpriority must agree with the consumer

When a <link rel="preload"> carries fetchpriority but the consuming element carries a different value, the request executes with the preload’s priority (it fired first) and the element’s hint is ignored for the network fetch. Keep the two declarations identical to avoid confusing future audits — and keep imagesrcset/imagesizes mirrored exactly, or the preload and the element fetch different URLs and you pay for both.

Hints are advisory, and Save-Data can override them

Engines may discount hints under memory pressure, data-saver modes, or heuristics of their own. Chromium, for instance, reserves the right to cap speculative work when Save-Data: on is present. Treat fetchpriority as a statistical improvement across your traffic, not a guaranteed per-session behaviour — and never encode application logic (ordering assumptions, race expectations) against a hint being honoured.

Demoting a script does not defer its execution

fetchpriority="low" on an async script changes fetch scheduling only; the moment the bytes arrive, the script still parses and executes on the main thread at the same point it otherwise would. If the goal is protecting interactivity rather than bandwidth, you need defer, type="module", or an idle-time loader — the hint alone will not stop a 400 KB bundle from blocking the main thread the instant it lands.

High-band inflation

Inside a band, requests are dispatched in discovery order and share bandwidth. Promoting six resources to High recreates the original contention one band higher, with the added cost that genuinely critical requests now have more neighbours. Audit pages quarterly for hint creep: the value of every high decays with each additional one.


FAQ

Is fetchpriority the same thing as preload?

No. Preload changes when a resource is discovered; fetchpriority changes where an already-discovered request sits in the scheduler queue. Preload creates a request the parser has not reached yet, while fetchpriority reorders requests relative to each other. They solve different problems and are frequently combined on the same <link> element.

Does fetchpriority=low delay when the request is sent?

Not directly. The request is created at the normal discovery moment but enters a lower scheduler band, so it only receives bandwidth after higher-band requests are dispatched. Under low contention it may still start immediately; under contention it waits. This is different from loading="lazy", which withholds the request entirely until the element nears the viewport.

Why does the Priority column not change after I add the attribute?

The most common causes are a typo (fetchPriority is the DOM property; the HTML attribute is all-lowercase fetchpriority), placing the attribute on an element type the browser ignores it on, a hint that requests the band the resource already occupies, or an HTML-rewriting layer such as a CDN optimizer stripping unknown attributes before the browser sees them.

Can fetchpriority make a request jump ahead of render-blocking CSS?

No. Hints shift a request within the range available to its resource type; they do not outrank the top band reserved for the document and render-blocking stylesheets. An image with fetchpriority="high" competes with other high-band requests, not with the stylesheet that gates first paint.

How many fetchpriority=high hints should a page carry?

Treat one to three as the working budget. Every additional high hint dilutes the ones already present, because requests inside the same band are dispatched in discovery order and share bandwidth. A page where six images are all high behaves almost identically to a page with no hints at all.


Related