Adaptive Loading using Client Hints and the Network Information API helps you deliver faster experiences. Learn how to tailor assets to real user conditions.
When I first tried to make our main dashboard "fast," I assumed that shipping the same high-resolution assets to everyone was fine as long as I used a CDN. Then I looked at our RUM data from a region with spotty 3G connectivity; the Largest Contentful Paint (LCP) was sitting at an abysmal 6 seconds. That realization shifted my entire approach to Adaptive Loading.
We aren't just building for fast fiber connections anymore. If you want to improve your Core Web Vitals, you have to stop treating all users as if they’re on a MacBook Pro plugged into Ethernet.
The goal of Adaptive Loading is simple: deliver the highest quality experience the user's current environment can actually handle. If a user is on a slow, high-latency connection, you shouldn't be forcing a 2MB hero image or a heavy JavaScript bundle down their throat.
We initially tried just checking navigator.connection.effectiveType. It seemed easy enough, but we quickly ran into issues with inconsistencies between browsers—especially on iOS, where the Network Information API is notoriously limited. We realized we needed a two-layered approach: server-side hints for initial delivery and client-side logic for runtime adjustments.
Client Hints are the most reliable way to get device and network data to your server before the request is even completed. By using the Accept-CH header, you tell the browser which specific details you need, like ECT (Effective Connection Type) or Downlink.
Here is how you configure your server headers to start receiving this data:
HTTPAccept-CH: Sec-CH-UA-Full-Version-List, Sec-CH-RTT, Sec-CH-ECT Vary: Sec-CH-RTT, Sec-CH-ECT Critical-CH: Sec-CH-RTT
Once your server sees these, you can make smarter decisions before rendering the HTML. For example, if Sec-CH-ECT comes in as 2g or 3g, you can immediately drop your hero image quality or skip non-essential heavy components. I’ve found that combining this with Client Hints for Adaptive Loading: A Modern Performance Guide makes the server-side delivery much more resilient.
While headers are great for the initial document request, they don't help you with assets requested via client-side JavaScript. This is where the Network Information API comes into play.
I use this pattern to conditionally load heavy modules:
JAVASCRIPTconst connection = navigator.connection || navigator.mozConnection || navigator.webkitConnection; function shouldLoadHeavyResource() { if (!connection) return true; // Default to loading if we can't tell // Don't load if the user is on a slow connection or has data saver on if (connection.saveData || connection.effectiveType === CE9178">'2g') { return false; } return true; } if (shouldLoadHeavyResource()) { import(CE9178">'./heavy-chart-library.js').then(module => { module.init(); }); }
This approach has been a game-changer for our Core Web Vitals. By deferring non-critical execution, we’ve seen LCP improvements of around 400ms on slower devices.
It's tempting to try and track everything, but complexity kills performance. Here’s how I think about the trade-offs:
| Strategy | Implementation | Best For | Reliability |
|---|---|---|---|
| Client Hints | Server Headers | Initial Page Load | High |
| Network Info API | Client JS | Dynamic/Lazy Loading | Medium |
| Media Queries | CSS/HTML | Responsive Images | Very High |
| Fetch Priority | HTML Attributes | Resource Ordering | High |
I often pair these with the Fetch Priority API: Optimize Resource Prioritization for Core Web Vitals to ensure that once I decide to load a resource, it actually gets the bandwidth it needs to finish quickly.
One thing that burned us early on: don't rely solely on the Network Information API for critical layout decisions. If the browser doesn't support it, or if the user has privacy extensions that spoof the network state, your app might fail to render entirely. Always provide a sensible default.
We also had to learn that "fast" is subjective. Sometimes, a slightly lower-quality image is "better" than a perfectly sharp image that takes 5 seconds to load. We started using a 1.5x pixel density for mobile and 2x for desktop, which cut our payload size significantly without noticeable visual degradation.
If you’re looking to dive deeper into how this impacts the overall loading flow, check out Adaptive Loading Strategies: Building Self-Healing Web Performance. It covers the architectural shifts needed to move beyond simple performance tuning.
I’m still not entirely happy with how we handle "network flapping"—where a user jumps from 4G to 3G while the page is loading. Right now, we just stick with the initial detection, but I’m experimenting with change event listeners on the navigator.connection object to see if we can prune secondary resource requests mid-load. It's messy, but that's the reality of engineering for the open web.
Does the Network Information API work on Safari? Currently, no. Apple has held off on implementing it due to privacy concerns regarding fingerprinting. You must treat it as an enhancement, not a requirement.
Will Client Hints break my CDN caching?
Yes, if you aren't careful. You must include the Vary header in your server responses to ensure the CDN caches different versions of your assets based on the hints received.
Is it worth the effort? For high-traffic sites where a 100ms improvement correlates to real revenue, absolutely. For a low-traffic marketing site, simpler responsive images might be enough.
Web Performance starts with a responsive main thread. Learn how to use Web Workers and MessageChannel to offload heavy tasks and improve your Core Web Vitals.
Read moreMaster bundle size optimization by auditing your dependency graph. Learn to strip unused code, use subpath imports, and enforce performance budgets today.