Learn how to use Client Hints and Accept-CH for adaptive loading. Optimize your assets based on device capability to improve Core Web Vitals and speed.
Last month, I spent about three days debugging a massive LCP (Largest Contentful Paint) regression that only seemed to hit users on mid-tier Android devices. After digging through our logs, I realized we were shipping high-resolution 4K hero images to users who couldn't even render them, wasting bandwidth and blocking critical CSS.
If you’re still relying on brittle user-agent sniffing to serve assets, you’re likely over-delivering data to mobile users or under-serving your high-end desktop traffic. Client Hints offer a cleaner, more robust way to handle content negotiation by letting the browser explicitly tell the server what it needs.
User-Agent (UA) strings are a mess. They are historically bloated, unreliable, and often intentionally spoofed by browsers to maintain compatibility. Trying to parse a UA string to guess a device’s memory, screen width, or network speed is a losing game.
Instead, we want to move toward Adaptive Loading where the server knows exactly what the client can handle before it even sends the first byte of the image or script. When I implemented Client Hints, I saw our image payload size drop by roughly 1.8x for mobile users, which directly improved our LCP scores.
To get started, your server needs to signal which "hints" it wants the browser to send. You do this using the Accept-CH response header.
In your Nginx or Node.js middleware, add the following header:
HTTPAccept-CH: Sec-CH-Viewport-Width, Sec-CH-DPR, Sec-CH-Width, Sec-CH-Device-Memory
Once the browser sees this on the initial request, it will include these values as headers in subsequent requests. It’s important to note that for the very first request, the browser won't have these hints yet. This is where a bit of architecture planning comes in.
We initially tried to handle everything at the edge using Cloudflare Workers. It worked well, but we hit a snag: if the browser doesn't have the hints stored, you have to provide sensible defaults.
| Feature | UA Sniffing | Client Hints |
|---|---|---|
| Accuracy | Low | High |
| Maintenance | High (regex hell) | Low (declarative) |
| Performance | Poor | Excellent |
| Privacy | Low | High (Reduced UA) |
If you ignore the "first request" gap, you might end up with a layout shift because the server sent the wrong image size before it knew the viewport width. We solved this by using a small, base-level image and then triggering a re-fetch or using srcset with the hint-provided data. If you’re struggling with similar layout issues, you might want to look into Adaptive Loading Strategies: Building Self-Healing Web Performance to handle those edge cases gracefully.
Once your server receives these headers, you can use them to build dynamic URLs. For example, if you see Sec-CH-DPR: 2.0, you know to serve a 2x image.
JAVASCRIPT// Example in a Node.js/Express environment app.use((req, res, next) => { const dpr = req.get(CE9178">'Sec-CH-DPR') || 1.0; const viewportWidth = req.get(CE9178">'Sec-CH-Viewport-Width') || 390; // Logic to select image size from your CDN req.imageVariant = selectBestVariant(dpr, viewportWidth); next(); });
This approach keeps your logic decoupled from the browser version. It’s significantly easier to maintain than a massive library of device profiles.
The goal here isn't just "cleaner code"—it's Performance Optimization. By ensuring that we don't fetch heavy resources unnecessarily, we reduce the total bytes downloaded. This helps keep the main thread free for critical tasks.
If you find that your backend is still the bottleneck despite these frontend optimizations, it’s worth investigating the Server-Timing API for INP Optimization: Debugging Backend Latency. Sometimes, what looks like a frontend performance issue is actually the server taking too long to generate the response that includes these hints.
Don't use this for everything. Prioritize your hero images, critical fonts, and major JS bundles. If you try to optimize every single icon or small asset, you’ll end up with cache fragmentation on your CDN. Remember that every unique set of headers creates a new cache key.
We also found that combining this with Resource Prioritization Strategies: Using Fetch Priority for Speed gave us the best results. We used Client Hints to pick the right file, and Fetch Priority to ensure the browser downloaded it immediately.
Is this perfect? No. You still have to deal with browsers that don't support specific hints and the initial request latency. I’m still experimenting with Critical-CH (the Critical-CH header) to force a browser to retry a request if the hints are missing, but it comes with a performance penalty.
If I were starting this again, I’d spend more time auditing our CDN cache hit rates before rolling out the headers. It’s easy to accidentally destroy your cache efficiency if you aren't careful with how your origin server varies content. Start small, measure the impact on your real-world users, and don't assume every hint will be available in every request.
Learn to eliminate critical request chains and boost Core Web Vitals. Discover how precise resource prioritization and preload scanning improve load times.
Read moreMaster Document Policy and Early Hints to slash critical path latency. Learn how to control browser network scheduling and optimize your resource prioritization.