Deprioritizing Below-the-Fold Images with fetchpriority=low
Your product grid or carousel fires a dozen image requests the moment the page loads, and they share the high-bandwidth window with the hero, the critical API call, and your fonts — inflating LCP even though none of those grid images is visible yet.
Root cause: near-viewport images fetch eagerly at competitive priority
The browser cannot know your layout’s fold before layout runs, so at parse time it fetches every <img> it discovers. Images the engine suspects are in or near the viewport get boosted treatment once layout confirms position, but the suspicion is generous: Chromium’s lazy-loading proximity margins extend well past the visible area (on slow connections, multiple viewport heights), and images inside a horizontally clipped carousel occupy in-viewport coordinates even when they are invisible to the user. The result is a burst of eight to fifteen image requests in the first 500 ms, all competing for the same connection’s congestion window as the resources that actually gate first paint.
This contention is a bandwidth problem, not a request-count problem. On HTTP/2 and HTTP/3 a single connection multiplexes everything, and the priority band of each stream decides who gets bytes when the pipe is full. A 4 MB grid of thumbnails at default image priority takes a real, measurable bite out of the transfer window your LCP image needs — commonly 300–900 ms of added fetch time for the hero on a throttled mobile profile.
The instinctive fix, loading="lazy" on everything below the fold, only half works — because it is a different mechanism solving a different problem. Lazy loading is deferred fetch: the request is not issued until the element approaches the viewport. fetchpriority="low" is a priority shift: the request is issued at the normal moment but placed in a lower scheduler band, receiving bandwidth only after higher bands are satisfied. Deferral eliminates requests that may never be needed, at the cost of scroll-time latency and a proximity margin you do not control; demotion keeps the bytes flowing during idle bandwidth, invisible to the scrolling user, but still downloads everything. Treating them as interchangeable produces the two classic failure modes: lazy-loading a carousel (slides never leave the viewport region, so nothing is deferred — or worse, slide transitions stutter waiting for a just-triggered fetch) and demoting far-below-fold galleries that should simply not load at all on bounce-heavy pages.
The attribute-level mechanics matter here too: low on an image whose computed priority is already Low is a no-op, which is why demotion pays off precisely on the near-fold and in-viewport-but-hidden images the heuristic boosts.
Minimal reproduction
A storefront above-the-fold hero plus a six-image “featured” row that starts 40 px below the fold — close enough that lazy loading’s margin fetches all six eagerly anyway:
<!-- BEFORE: all seven images enter the queue together; layout boosts the
near-fold row, and the hero shares its bandwidth window six ways -->
<img src="/img/hero.avif" width="1600" height="900" alt="Autumn collection hero">
<div class="featured-row">
<img src="/img/f1.webp" loading="lazy" width="400" height="300" alt="Wool coat">
<!-- …f2 through f6 identical: lazy, but inside the proximity margin,
so they fetch immediately at boosted priority anyway -->
</div>
<!-- AFTER: hero promoted; featured row demoted rather than (ineffectively)
deferred — requests still start early but only consume bandwidth the
critical set is not using -->
<img src="/img/hero.avif" width="1600" height="900" alt="Autumn collection hero"
fetchpriority="high">
<div class="featured-row">
<!-- Scheduling rationale: near-fold images defeat lazy loading's margin,
so demotion is the only lever that actually moves them out of the
hero's bandwidth window -->
<img src="/img/f1.webp" fetchpriority="low" decoding="async"
width="400" height="300" alt="Wool coat">
<!-- …f2 through f6 identical -->
</div>
For content genuinely far below the fold — a footer gallery two screens down — combine the mechanisms: loading="lazy" to skip the fetch for bouncing visitors, and accept that fetchpriority contributes little there because the deferred request fires close to the viewport, where the browser wants it promptly anyway.
Priority shift vs deferred fetch
Deterministic fix protocol
- [ ] 1. Quantify the contention window. In the DevTools Network panel (Img filter, Priority column on, Fast 4G throttling), hard-reload and note every image request that starts before the LCP image finishes. Sum their transferred bytes within that window — this is the budget you are about to reclaim.
- [ ] 2. Classify each offending image into three zones. Zone A: visible in the first viewport (leave at default; promote only the LCP element). Zone B: near-fold or in-viewport-but-hidden (carousel slides, tab panels, first grid row) — these defeat lazy loading’s proximity margin. Zone C: far below the fold (more than ~two viewport heights down).
- [ ] 3. Apply
fetchpriority="low"to Zone B. Do not addloading="lazy"here; deferral either will not trigger (in-viewport slides) or will re-fetch at an awkward scroll moment. Adddecoding="async"so decode work also stays off the critical path. - [ ] 4. Apply
loading="lazy"to Zone C, without a priority hint. The deferred request fires near the viewport where prompt fetching is desirable; alowhint there mostly adds risk of visible pop-in. Keep explicitwidth/heighton every image so deferral cannot cause layout shift. - [ ] 5. Sweep for template-level conflicts. Grep templates for images that ended up with both
fetchpriority="low"andfetchpriority="high"variants from component props, and for CMS plugins stampingloading="lazy"onto Zone B. One template usually renders all three zones — parameterize the attribute, don’t hardcode it. - [ ] 6. Verify the shift. Reload and confirm Zone B images show Low in the Priority column and their waterfall bars now transfer after the LCP image completes. The LCP image’s own bar should visibly shorten — that is the reclaimed bandwidth.
- [ ] 7. Verify nothing regressed on scroll. Throttle to Slow 4G, load, then scroll steadily to the footer. Zone B should already be painted (it downloaded in the idle gap); Zone C should pop in within one frame of entering the margin. If Zone C lags, its images are too heavy for scroll-time fetch — that is a compression problem, not a scheduling one.
Before/after metrics
Category page, 1 hero + 12 grid images (3.1 MB total imagery), Fast 4G emulation, cold cache, median of 9 runs. Steps 1–6 applied; grid row one demoted (Zone B), rows two to four lazy-loaded (Zone C).
| Metric | Before | After | Delta |
|---|---|---|---|
| Image bytes transferred during LCP window | 1,480 KB | 190 KB | −87% |
| LCP image fetch duration | 1,120 ms | 460 ms | −59% |
| LCP (lab, p50) | 3.0 s | 1.9 s | −37% |
| Time to grid row 1 fully painted | 1.6 s | 2.1 s | +0.5 s (below fold, unobserved) |
| Image requests on load (bounce case) | 13 | 7 | −6 requests |
| CLS | 0.01 | 0.01 | unchanged |
The trade is explicit in row four: the demoted images finish half a second later — behind the fold, where no one is looking. That is the entire bargain of fetchpriority="low": spend invisible seconds to buy visible ones.
FAQ
If loading=lazy already defers offscreen images, why bother with fetchpriority=low?
Because lazy loading has a coverage gap: images inside the browser’s proximity margin — typically everything within roughly one to three viewport heights of the fold — are fetched during initial load anyway, at normal image priority. fetchpriority="low" is the only tool that demotes those near-fold fetches, and it also covers slotted carousel frames that sit inside the layout viewport where lazy loading never triggers at all.
Will fetchpriority=low make my gallery visibly slower for users who scroll immediately?
Rarely, and only under contention. The requests are issued at the normal time and simply yield bandwidth while critical resources transfer; on a page that finishes its critical set within the first second, demoted images reclaim full bandwidth almost immediately. If fast-scroll views matter commercially, keep the first below-fold row at default priority as a buffer and demote everything after it.
Should hidden carousel slides use fetchpriority=low or loading=lazy?
Slides in the same viewport-level position as the visible slide will not trigger lazy loading, because proximity-based deferral considers them near or inside the viewport. Use fetchpriority="low" on slides two and three so they fetch cheaply in the background, and lazy-load slides four onward only if the carousel supports jumping ahead; otherwise demotion alone gives smoother slide transitions.
Related
- The fetchpriority Attribute & Priority Hints — parent guide: how low and high shift each resource type’s computed priority
- fetchpriority=high Not Working on Your LCP Image — the promotion-side counterpart when the hero itself misbehaves