Build a priority-based data fetching system
Above-the-fold + critical data: high priority, fetched immediately. Below-the-fold: low priority, fetched on idle / when visible. Use `priority` flag on requests, leverage `fetchpriority` attribute for resources, a queue that releases high before low, IntersectionObserver to upgrade priority when a component nears viewport, AbortController to cancel low-priority work when high-priority arrives.
A priority-based fetcher decides what's important to fetch first so the user sees critical UI fast even when the network is constrained. Useful on mobile and feed-heavy apps.
1. The model
Two or three tiers:
- Critical / above-the-fold — hero, header data. Fetch immediately.
- Normal / visible-soon — content the user is about to see.
- Low / background — analytics, prefetching, below-the-fold extras.
2. Implementation — priority queue + concurrency cap
class PriorityFetcher {
constructor({ concurrency = 6 } = {}) {
this.concurrency = concurrency;
this.running = 0;
this.queues = { high: [], normal: [], low: [] };
}
fetch(url, options = {}, priority = "normal") {
return new Promise((resolve, reject) => {
const controller = new AbortController();
const job = { url, options: { ...options, signal: controller.signal }, resolve, reject, controller };
this.queues[priority].push(job);
this._dequeue();
job.cancel = () => controller.abort();
});
}
_dequeue() {
while (this.running < this.concurrency) {
const job = this.queues.high.shift() || this.queues.normal.shift() || this.queues.low.shift();
if (!job) return;
this.running++;
window.fetch(job.url, job.options)
.then((res) => job.resolve(res))
.catch((err) => job.reject(err))
.finally(() => { this.running--; this._dequeue(); });
}
}
cancelLow() {
for (const j of this.queues.low) j.controller.abort();
this.queues.low = [];
}
}3. Browser primitives that help
fetchpriority
For <img> and <link rel="preload">:
<img src="hero.jpg" fetchpriority="high">
<link rel="preload" as="image" fetchpriority="high" href="hero.jpg">The browser bumps these in its internal scheduler.
Priority Hints for fetch
fetch(url, { priority: "low" }); // chromium-supported hintHints, not guarantees.
requestIdleCallback
Schedule low-priority work in idle time:
requestIdleCallback(() => prefetchNextPage(), { timeout: 2000 });4. Upgrade priority on visibility
A component scrolling toward viewport should bump its data fetch from low to high:
const ref = useRef();
useEffect(() => {
const obs = new IntersectionObserver(([entry]) => {
if (entry.isIntersecting) fetcher.upgrade(jobId, "high");
}, { rootMargin: "200px" });
obs.observe(ref.current);
return () => obs.disconnect();
}, []);5. Cancel low-priority on high
When a critical request comes in and we're saturated, cancel low-priority in-flight work to free a slot:
fetcher.cancelLow();
fetcher.fetch(criticalUrl, {}, "high");AbortController kills the actual request (frees both browser and server).
6. Integration with React Query / SWR
Both have notions of priority via their fetcher functions:
- Use the priority fetcher as the
queryFn. - Mark queries as
enabled: isVisiblefor low-priority items. - Use
prefetchQueryfor warm-cache patterns.
7. The actual wins
This pattern matters most:
- On constrained networks (3G, weak Wi-Fi).
- For complex pages with many parallel requests.
- For feed-like UIs where below-the-fold work can starve above-the-fold.
On a fast connection with 5 requests, priority makes little difference. Measure before adding complexity.
8. Patterns to avoid
- Promise.all on everything — every request fires in parallel; no prioritization possible.
- Sequential everything — too slow when parallelism would be fine.
- Custom priority where the browser already prioritizes — for HTTP/2, the browser does some of this; don't fight it.
Interview framing
"Three tiers — critical, normal, low — backed by a priority queue with a concurrency cap. Each fetch goes into its tier's queue; the dispatcher drains high first, then normal, then low. AbortController per job lets us cancel low-priority in-flight work when a high-priority request comes in. IntersectionObserver upgrades a job's priority when its component nears the viewport. Browser primitives help: fetchpriority on resources, { priority: 'low' } on fetch, requestIdleCallback for background work. The pattern earns its keep on constrained networks and feed-heavy UIs — on a fast pipe with few requests, the complexity isn't worth it. Measure first."
Follow-up questions
- •Why use AbortController to cancel low-priority work?
- •What's fetchpriority and when does it actually help?
- •How would this integrate with React Query?
- •When is the complexity NOT worth it?
Common mistakes
- •All requests fired in parallel with no prioritization.
- •Manual priority that fights the browser's scheduler.
- •No cancellation — low priority requests keep running.
- •Premature complexity when the network isn't the bottleneck.
Performance considerations
- •Critical for mobile and feed-heavy apps. Combine with HTTP/2 multiplexing — the browser opens fewer connections than HTTP/1.
Edge cases
- •Low-priority request becomes high after scroll.
- •Network drops with high-priority in flight.
- •Server-side rate limit returns 429 — back off, don't retry rapidly.
Real-world examples
- •News feeds: above-the-fold critical, below-the-fold deferred.
- •Image galleries: visible images first, prefetch on hover.