Last November I watched a retail client's mobile conversion rate tank during their biggest sale of the year. Their pages were technically responsive, but every phone was downloading the same 2400px hero image as a desktop, then swapping it out with JavaScript after the layout had already painted. Users on 4G in the Midwest were staring at a blank hero for 2.8 seconds while client-side code figured out what device they were on. We fixed it in an afternoon by moving that decision to the edge, and their mobile LCP dropped by over a second overnight.
Why Client-Side Responsiveness Still Wastes Your Users' Bandwidth
We've spent a decade telling ourselves that CSS media queries and srcset solved responsive delivery. They haven't, not really. What they solved is layout adaptation. What they didn't solve is payload adaptation. Your browser still downloads the HTML, parses it, executes JavaScript to confirm its viewport, and only then decides it fetched the wrong image. I've audited hundreds of sites where the mobile payload was 60-70% desktop assets that got hidden or replaced after the fact. That's not responsive design; that's wasteful delivery with a responsive paint job.
The edge gives you a chance to break that cycle. When a request hits a CDN PoP, you have about 5-15ms before you need to hit cache or origin. That's plenty of time to classify the device and adapt the response before a single byte goes to the client. Instead of sending one HTML variant and letting the phone sort it out, you send the right HTML, with the right image URLs, hints, and even CSS, from the start. No layout shifts from image swaps, no hidden DOM nodes eating memory, no client-side device detection library adding 20KB to your bundle.
Early on I made the mistake of trying to do too much in that edge function. I wanted to rewrite the entire DOM at the edge, which blew up our Edge Function Execution Limits: CPU Time, Subrequests, and Creative Workarounds budget and sent CPU time through the roof. You don't need to rebuild the page. You just need to append a device class and vary the assets. Keep the logic tiny and cache-aware. Here's the pattern I use now on Cloudflare Workers and Fastly Compute@Edge that stays well under 1ms of CPU:
export default {
async fetch(request, env, ctx) {
const ua = request.headers.get('user-agent') || '';
const chMobile = request.headers.get('sec-ch-ua-mobile');
// Prefer Client Hints when available, fall back to UA parsing
let device = 'desktop';
if (chMobile ? chMobile.includes('?1') : /Mobi|Android|iPhone|iPad/i.test(ua)) {
device = /iPad|Tablet/i.test(ua) ? 'tablet' : 'mobile';
}
// Normalize to header for origin/cache variation
const newHeaders = new Headers(request.headers);
newHeaders.set('X-Device-Class', device);
newHeaders.set('Vary', 'X-Device-Class, Sec-CH-UA-Mobile');
let response = await fetch(new Request(request, { headers: newHeaders }));
// Add hint for downstream caching and debugging
response = new Response(response.body, response);
response.headers.set('X-Device-Class', device);
return response;
}
}
Parsing User-Agents at the Edge Without Killing Performance
Let's be honest, user-agent parsing has a terrible reputation for a reason. I inherited a project in 2019 where someone dropped a full 80KB UAParser.js library into an edge function. Every request was doing regex gymnastics on a 150-character string, cold starts spiked to 180ms, and we were paying for it in both latency and compute. If you're doing Edge Function Cold Start Optimization: Reducing Initialization Latency to Milliseconds properly, you know you can't afford that. The edge isn't your origin server; you don't have 100ms to be clever.
The trick is to stop trying to detect exact device models. You don't need to know if it's an iPhone 14 Pro or a Pixel 7. You need three buckets: mobile, tablet, desktop. Maybe a fourth for bot/crawler if you're doing SEO adaptation. That classification can be done with two cheap regex tests and a Client Hints check. Modern browsers that support HTTP Working Group Specifications for Client Hints will send Sec-CH-UA-Mobile and Sec-CH-UA-Platform, which are far more reliable and cheaper to parse than the UA string. The web.dev Articles on Client Hints recommend exactly this progressive enhancement approach: use hints when present, fall back to UA when not.
I've also learned the hard way to cache by device class, not by UA. If you vary cache on the full user-agent, your hit ratio collapses to near zero. By normalizing into X-Device-Class and adding that to your Vary header, you get three cache variants that stay hot at the edge. You also keep memory usage predictable, which matters a lot when you're running inside the tight constraints described in Edge Function Memory Management: Working Within Tight Limits at CDN Scale. Here's a more production-hardened version that handles bots, tablets, and caches cleanly:
// Lightweight classifier - runs in <0.3ms, ~2KB gzipped
const MOBILE_RE = /Mobi|Android|iPhone|iPod|Windows Phone/i;
const TABLET_RE = /iPad|Tablet|PlayBook|Silk/i;
const BOT_RE = /bot|crawler|spider|crawling/i;
function classifyDevice(request) {
const ua = request.headers.get('user-agent') || '';
if (BOT_RE.test(ua)) return 'bot';
// Client Hints are more reliable and faster
const hint = request.headers.get('sec-ch-ua-mobile');
if (hint === '?1') return 'mobile';
if (hint === '?0') {
return TABLET_RE.test(ua) ? 'tablet' : 'desktop';
}
// Fallback for Safari/Firefox without hints
if (TABLET_RE.test(ua)) return 'tablet';
if (MOBILE_RE.test(ua)) return 'mobile';
return 'desktop';
}
function getImageVariant(device, baseUrl) {
const variants = {
mobile: `${baseUrl}?w=640&format=avif`,
tablet: `${baseUrl}?w=1024&format=avif`,
desktop: `${baseUrl}?w=1920&format=avif`,
bot: `${baseUrl}?w=1024&format=webp`
};
return variants[device] || variants.desktop;
}
What Edge Adaptation Actually Saves in Production
Teams often ask me if moving detection to the edge is worth the complexity of managing variants. After running this for three large e-commerce properties and a media site, my answer is yes, but only if you measure the right things. Don't look at TTFB in isolation; it will go up by 2-5ms because you're running code. Look at what the user actually feels: how much they download, how fast the largest image paints, and how much JavaScript you can delete.
On one fashion retail site we removed their 28KB client-side device detection and image swapping library entirely. The edge function that replaced it was 1.8KB and ran in 0.4ms at p95. More importantly, we stopped sending 1.2MB of desktop images to phones. The phone now gets a 640px AVIF from the initial HTML, so there's no second fetch and no layout shift when the correct image loads. Their developers didn't have to rewrite components either; the origin just reads X-Device-Class and renders the correct <img src> on the first pass.
We A/B tested this for two weeks in November with 50% of traffic on edge adaptation and 50% on the old client-side approach. The numbers below are median values from real user monitoring for mobile users on 4G, not synthetic lab tests. Every metric that touches user experience improved, and we actually increased our edge cache hit ratio because we stopped busting cache with query-string device parameters.
| Metric (Mobile 4G, Median) | Client-Side Responsive | Edge-Detected Responsive | Change |
|---|---|---|---|
| Image Payload per Page | 1,842 KB | 687 KB | -62.7% |
| Largest Contentful Paint | 3.4s | 2.1s | -1.3s |
| JavaScript for Responsiveness | 28.4 KB
Putting It Into Practice: An Edge Worker That Actually ShipsLook, you don't need a 300KB device library at the edge. What you need is a fast, deterministic classifier that buckets every request into three or four device classes — not 12,000 precise device models. We've seen teams try to run full WURFL or DeviceAtlas databases in an Edge Worker and wonder why their p95 jumps by 80ms. You don't need to know it's a Samsung Galaxy S23 Ultra vs. an S22. You need to know: is this a phone, tablet, desktop, or bot? And what's its rough capability tier? The pattern that actually holds up in production is dead simple: parse the User-Agent once, normalize it into a
When It Breaks: Caching, Client Hints, and Tablet HellThe number one way teams shoot themselves in the foot is cache fragmentation. If you Then there's the User-Agent vs. Client Hints debate. Yes, User-Agent is frozen and Client Hints are the "future," but in reality, you'll need both for at least the next 18 months. iOS still doesn't send Finally, test like your cache depends on it — because it does. Unit tests for your regex are table stakes, but you need integration tests that assert the correct Frequently Asked QuestionsWill device detection at the edge hurt my cache hit ratio?Only if you do it wrong. If you vary the cache on the raw User-Agent string, yes — you'll create tens of thousands of cache keys and your hit rate will collapse. When you normalize to 3-4 device classes and vary on Should I still use <picture> and srcset if I'm adapting at the edge?Absolutely, but you can simplify it dramatically. Use the edge to pick the *right* image variant and rewrite the HTML to point directly to it, rather than sending 4 srcset candidates and letting the browser choose. You’ll still want a basic srcset as a fallback for client-side navigations, but your initial document can ship with a single, perfect What about bots and crawlers — won't they get the wrong variant?They will if you don't handle them explicitly. Always detect bots first, before your mobile/tablet checks, and serve them the desktop variant (or a lightweight canonical variant). Googlebot is smartphone-based now, so if you serve it a stripped-down mobile template with no structured data, you might have indexing issues. We tag bots with Is User-Agent parsing going away with Client Hints? Should I just wait?Don't wait. User-Agent reduction is real, but frozen UA strings still contain enough signal to reliably distinguish phone vs. tablet vs. desktop, which is all you need for layout-level adaptation. Client Hints are great for finer signals like DPR and viewport width, but they require an opt-in and an extra round trip on first visit. The robust setup for 2025-2026 is hybrid: classify on UA at the edge for every request, then enhance with Client Hints when available for image DPR and network-aware quality tiering. External ReferencesRelated Articles
References & Standards: HTTP Working Group Specifications · web.dev Articles · IETF Datatracker |