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:
- Hints do not affect discovery. A request must exist before it can be reprioritized. An image referenced from a CSS
background-imageor 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. - Hints do not cross the render-blocking ceiling. The document itself and render-blocking CSS occupy the top band unconditionally. No
fetchpriorityvalue places a subresource above them. - 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.
How the hint reaches the network stack
It helps to know where in the pipeline the attribute is consumed, because that explains most of the failure modes later in this guide. In Chromium the sequence is:
- The preload scanner or the parser creates a candidate request. The scanner runs ahead of the main parser over raw bytes and already reads
fetchpriority, which is why the attribute works on markup the parser has not formally reached. - A priority is computed once, at request creation. The fetcher combines the resource type, the
asvalue on a preload, whether the element is render-blocking, the scanner’s guess at viewport position, and thefetchpriorityvalue into a single band. There is no later revision path from script: once the request exists, its band is fixed apart from the browser’s own in-viewport boost. - The load scheduler decides whether the request may go out now. While render-blocking work is outstanding, Chromium runs in a restricted mode that allows only a couple of non-critical requests in flight at a time. Requests at Medium and above are exempt from that throttle. This is the part most people miss: promoting an image is not only about queue order, it is about escaping the throttle entirely.
- The band becomes a protocol signal. Chromium maps the computed priority onto the urgency value in the
Priorityrequest header and onto its HTTP/2 stream signalling, so the hint travels to the CDN as well as governing local ordering. Whether the edge acts on it is a separate question — see HTTP/2 priority trees versus the HTTP/3 Priority header.
Two consequences follow. First, a hint that arrives after step 2 is worthless, which is why the DOM-property ordering in Step 6 below matters so much. Second, the biggest single win from fetchpriority="high" on an image is often invisible in the Priority column: the request skips the non-critical throttle and is dispatched during the window when the browser is otherwise only fetching the stylesheet.
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 |
<link rel="modulepreload"> |
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.
Notice that the transfer itself also shortens, from 1,250 ms to 1,030 ms. That is not the band changing the link speed; it is the promoted request no longer sharing the connection with the six thumbnail requests that used to overlap it. On a multiplexed HTTP/2 or HTTP/3 connection, bandwidth is divided among active streams, so removing competitors from the same window is worth as much as starting earlier. It also explains why demotions elsewhere on the page improve the promoted request without anyone touching it.
Where the in-viewport boost helps and where it does not
Chromium’s boost is genuinely clever and covers a lot of pages without any author intervention: the first few images the layout confirms inside the viewport get lifted out of Low automatically. It fails in three recognisable situations.
- The image is discovered after first layout. Anything injected by a carousel, a hydration pass, or a client-side router misses the window in which the boost would have mattered, because by then the boost and the hint would arrive at the same time.
- The layout is expensive. On a page where the stylesheet is large and the first layout lands at 900 ms, the boost lands at 900 ms too. The hint lands at 120 ms.
- The image is inside a container the layout has not sized yet. Web-component slots,
content-visibility: autoregions, and late-measured aspect ratios can all delay the confirmation the boost depends on.
If none of those apply — a static hero, in the initial HTML, above a light stylesheet — the boost may already be doing the job, and the hint will look like it changed nothing. That is a correct outcome, not a failed rollout.
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).
There is a subtler cross-engine trap in the demotion direction. Because Gecko historically fetches images more eagerly than Chromium, a page tuned by adding low to twenty gallery images can behave differently there: the images Chromium was already deferring are the ones Firefox was fetching, and the attribute changes the shape of the load rather than confirming it. If your traffic has a meaningful Firefox or Safari share, read the cross-engine comparison in Chrome vs Safari vs Firefox priority differences before assuming a Chromium waterfall generalises.
Implementation, step by step
Before writing a single attribute, decide which lever the request actually needs. Most disappointing rollouts are cases where the request needed earlier discovery or outright deferral, and got a priority hint instead. The tree below is the order to work through.
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.
Record three numbers alongside the screenshot: the LCP value, the start time of the LCP resource, and the number of requests that were in flight when it started. The third number is the one that tells you whether demotions are worth writing; if only two requests overlap your hero, there is nothing to demote and the promotion is the whole story.
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. Note that decoding="async" is unrelated to the network band — it governs the main-thread decode step after the bytes land — but it belongs on the same element, because a promoted hero that then blocks the main thread for 40 ms of synchronous decode gives back part of what the hint won. 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.
One caveat on responsive images: the hint applies to the element, not to a candidate. Whichever URL srcset and sizes resolve to inherits the promotion, so a mistuned sizes that selects the 1600w file on a phone now downloads a too-large file faster and earlier, which is worse than the unhinted version. Verify the selected candidate at each breakpoint before shipping the attribute.
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. The iframe case is the highest-leverage one on most commercial pages, because a third-party frame is not one request but a whole document load: its own HTML, its stylesheet, its fonts, its scripts, each inheriting a band from the frame’s own scheduler. Demoting the frame element demotes the tree.
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.
The same option is available on a Request object, which is the form to use when the call goes through a wrapper or a service worker:
// Scheduling rationale: constructing the Request carries the priority with
// the object, so any layer that forwards the Request (a retry wrapper, a
// service worker fetch handler) preserves the band instead of resetting it.
const req = new Request('/api/inventory', { priority: 'low' });
const res = await fetch(req);
Beacons are a special case worth calling out. navigator.sendBeacon() is already scheduled off the critical path and does not need a hint; a fetch() with keepalive: true does, because it is an ordinary High-priority fetch that happens to survive page unload. Analytics libraries that use keepalive fetches during the load — not just at unload — are a common hidden competitor for your hero image’s bandwidth.
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. A useful test: if the request’s start time is already close to the document’s, you have a priority problem; if it starts hundreds of milliseconds late, you have a discovery problem, and the hint alone will not help.
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.
The ordering rule is just as strict. Assigning src is what creates the request, so any priority you set afterwards is applied to nothing. The same applies to a dynamically inserted <link rel="preload">: set fetchPriority before appending the element to the head. Patterns for injecting hints safely at runtime are collected in dynamic hint injection via JavaScript.
Step 7 — Wire the hint through your framework, not around it
Most frameworks expose the hint under a different name, and hand-writing the attribute usually fights the abstraction rather than complementing it:
- Next.js —
next/imagetakes apriorityboolean that emits both a preload link andfetchpriority="high". Setting the raw attribute on a component that is also lazy by default produces the contradictory pair from the gotchas below; see fixing Next.js LCP image priority. - Nuxt —
<NuxtImg preload>and the head helpers generate the link; the attribute passes through as a normal prop. - Astro — the image component forwards unknown attributes, so
fetchpriority="high"on the element works, but thepriorityshorthand also emits the preload. - SvelteKit — the enhanced image preprocessor keeps arbitrary attributes on the emitted
<img>, and the data-loading directives are the place to classify fetches rather than the components.
Whatever the framework, the review rule is the same: exactly one element per route template may carry the promotion, and the code review that adds a second one should remove the first.
Verification workflow
DevTools Priority column
- 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.
- Confirm each hinted request landed in its intended band: the hero image at High from its first appearance, demoted iframes and fetches at Low.
- 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.
- 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.
The panel below is what a correct rollout looks like on the worked example from Step 2 through Step 4.
If your panel shows a hinted row with two priorities separated by an arrow, the browser changed its mind after the request was created — almost always the in-viewport boost firing on an image you did not hint. That is the signal to add the hint, because the boost is arriving late.
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.
Confirming the win in the field
Lab numbers on a throttled profile prove the mechanism works; they do not prove it helps your users, because the effect size depends on how contended their connections actually are. Ship the same two measurements to your analytics endpoint for a fortnight:
// Verification rationale: pair the LCP time with the LCP resource's own
// start time, so a regression can be attributed to discovery, queueing or
// transfer rather than showing up as one opaque number.
new PerformanceObserver((list) => {
const last = list.getEntries().at(-1);
const res = performance.getEntriesByName(last.url)[0];
navigator.sendBeacon('/rum', JSON.stringify({
lcp: Math.round(last.startTime),
lcpStart: res ? Math.round(res.startTime) : null,
lcpQueue: res ? Math.round(res.requestStart - res.startTime) : null,
protocol: res ? res.nextHopProtocol : null,
cached: res ? res.transferSize === 0 : null
}));
}).observe({ type: 'largest-contentful-paint', buffered: true });
Segment the result by cached before drawing conclusions. Warm-cache sessions will show no movement at all, and if they dominate your traffic they will dilute a real cold-load improvement into statistical noise. Segment by protocol too: the benefit of demotions is larger on a single multiplexed HTTP/2 or HTTP/3 connection, where all your streams genuinely share one bandwidth pool, than on a connection-limited HTTP/1.1 origin.
A worked example, end to end
A product listing page on a mid-range Android device over Fast 4G, before any hints:
| Request | Type | Band | Start | Finish |
|---|---|---|---|---|
document.html |
doc | Highest | 0 ms | 320 ms |
main.css |
css | Highest | 340 ms | 620 ms |
/api/listing |
fetch | High | 350 ms | 910 ms |
/api/recommendations |
fetch | High | 355 ms | 1,180 ms |
/api/beacon (keepalive) |
fetch | High | 360 ms | 640 ms |
hero-1600.avif |
img | Low → High | 690 ms | 1,940 ms |
6 × thumb-*.webp |
img | Low | 700 ms | 2,010 ms |
| chat frame + subtree | doc | High | 380 ms | 1,760 ms |
| LCP | 2,050 ms |
Three problems are visible without any further tooling. The hero starts at 690 ms because it waits for the layout boost. Four High-band requests — two APIs, a beacon, and a third-party frame — are already occupying the connection when it finally starts. And the six thumbnails, though correctly at Low, still overlap the hero’s transfer window because nothing above them is finishing quickly.
Four attribute changes later — high on the hero, low on the recommendations fetch, low on the beacon fetch, low on the chat iframe — the same load looks like this:
| Request | Type | Band | Start | Finish |
|---|---|---|---|---|
document.html |
doc | Highest | 0 ms | 320 ms |
main.css |
css | Highest | 340 ms | 620 ms |
hero-1600.avif |
img | High | 150 ms | 1,180 ms |
/api/listing |
fetch | High | 350 ms | 880 ms |
/api/recommendations |
fetch | Low | 900 ms | 1,620 ms |
/api/beacon (keepalive) |
fetch | Low | 950 ms | 1,240 ms |
6 × thumb-*.webp |
img | Low | 1,000 ms | 2,180 ms |
| chat frame + subtree | doc | Low | 1,200 ms | 2,600 ms |
| LCP | 1,290 ms |
The hero now starts at 150 ms, immediately after the preload scanner sees it, and finishes 760 ms earlier. Notice what got worse: the thumbnails finish 170 ms later and the chat frame 840 ms later. That is the trade being made explicitly, and it is the right one — nothing in that list is on the path to the metric the page is judged by. The failure mode to watch for is a demotion that is not actually deferrable, such as a fetch whose result the user is waiting on after an interaction; the queueing that shows up there is a real regression, and it appears in request queueing and stalled time rather than in LCP.
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, and the thresholds each mechanism uses in lazy loading and viewport-driven fetching.
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. The mismatch usually announces itself as a console warning; the patterns behind it are catalogued in debugging “preloaded but not used” warnings.
A service worker can silently reset the band
This is the most expensive gotcha on the list because it invalidates an entire rollout at once. When a fetch event handler responds with a new request built from the URL —
// BROKEN: rebuilding from the URL drops the priority the page asked for.
self.addEventListener('fetch', (event) => {
event.respondWith(fetch(event.request.url));
});
// CORRECT: forward the Request object, which carries priority, mode,
// credentials and destination with it.
self.addEventListener('fetch', (event) => {
event.respondWith(fetch(event.request));
});
— every hinted request on the page arrives at the network at the default priority. Your demotions vanish (everything is High again) and your promotion is lost. Audit any service worker that touches image or API traffic before concluding the attributes are not working.
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. The execution-order rules that actually control that are covered in script loading: async, defer and execution order.
Early Hints and fetchpriority can disagree
A 103 Early Hints response can start a preload before the HTML exists. The band that request gets comes from the as value and any fetchpriority in the header — not from whatever the eventual HTML says. If the header preloads the hero as=image without a hint, it lands at Low; the matching <img fetchpriority="high"> later in the document then finds an in-flight request it cannot promote. Carry the hint in the header too, or the early start buys you less than it should.
Cache hits and back/forward navigations flatten the effect
A request served from the disk cache completes in a few milliseconds and never meaningfully contends, so reordering it is close to a no-op. A back/forward-cache restore replays no requests at all. Both are good outcomes, but they mean the population in which your hint matters is narrower than your total traffic: cold first views, on contended connections. Measure there, or the change will look like it did nothing.
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.
The attribute can be stripped before the browser sees it
HTML minifiers, template sanitizers, ad-server rewriters, and CDN image optimizers all rewrite <img> tags, and some drop attributes they do not recognise or replace the element entirely with their own markup. View the served HTML, not your template, when a hint appears to have no effect — curl the URL and grep for the attribute. If the optimizer owns the tag, apply the hint through the optimizer’s own configuration instead.
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.
Does a service worker preserve the hint?
Only if you preserve it. A handler that calls fetch(event.request) forwards the original Request object and the priority travels with it. A handler that rebuilds the request from event.request.url creates a brand new fetch at the default High priority, discarding both your promotion and every demotion on the page.
Does the hint help when the response comes from cache?
Barely. A disk cache hit costs a few milliseconds and does not contend for bandwidth, so reordering it changes almost nothing. Priority Hints pay off on cold loads and first visits — which is also where LCP problems concentrate — so evaluate them on cold-cache traffic rather than on your own warm repeat loads.
How does fetchpriority relate to the HTTP Priority header?
In Chromium the computed priority feeds the urgency value sent in the Priority request header and the equivalent HTTP/2 signalling, so a promoted request also asks the server to schedule its response earlier. Whether that has any effect depends on the CDN or origin honouring the header. Treat protocol-level scheduling as a bonus on top of local ordering, never as the mechanism you are relying on.
Should every below-the-fold image get fetchpriority=low?
No, because most of them already sit at Low and the attribute changes nothing. Demotion is worth writing only where the default is High: fetch() calls, iframes, script and fetch preloads, and images the layout has already boosted. Blanket low attributes across a gallery add markup and no scheduling change.
Can I change a request’s priority after it has started?
No. The band is computed when the request is created, and there is no API to revise it afterwards; assigning fetchPriority to an element whose src is already set does nothing to the in-flight fetch. The only way to reprioritize is to abort and re-issue, which means paying for the bytes twice — which is why the ordering in Step 6 matters.
Does fetchpriority work on <video>, <audio> or CSS background images?
Not today. The attribute is defined for <img>, <link>, <script> and <iframe> plus the fetch() option. Media elements take preload="none|metadata|auto" instead, which is a discovery control rather than a band control, and a CSS background image has no element to carry an attribute — preload it with as="image" and put the hint on the link.
Related
- fetchpriority=high Not Working on Your LCP Image — the diagnosis protocol for when the promotion hint has no effect
- Deprioritizing Below-the-Fold Images with fetchpriority=low — the demotion half of the spec applied to galleries and carousels
- Lazy Loading & Viewport-Driven Fetching — when deferral is the better answer than demotion
- Chrome vs Safari vs Firefox: Priority Differences — how far a Chromium waterfall generalises
- Up: Core Browser Loading Mechanics & Priority Queues