Save-Data Is a Request, Not a Verdict: Serving Leaner Bundles Without Snobbery
Photo: Mr Addlepated , CC BY-SA 2.0, via Wikimedia Commons
There's a particular kind of optimism in shipping a 900KB JavaScript bundle to a phone on a £10-a-month plan with 5GB of data and one bar of signal on the 07:42 out of Leeds. The page will load. Eventually. Somewhere around Doncaster. And then the user will close it, because they've already spent a chunk of their allowance watching a skeleton screen animate.
So the idea of a lite build is sound. The trouble is that most attempts at it fall into one of two ditches: either you sniff the user agent and hand iPhone owners a degraded experience they never asked for, or you build a whole parallel app that drifts out of sync within a fortnight. This piece is about the middle path — reading the signals the browser actually gives you, shipping a leaner bundle that behaves, and measuring whether it made a blind bit of difference.
The header beats the guessing game
Start with Save-Data: on. It's a request header, sent by Chrome and a few others, and it means exactly one thing: the person on the other end has turned on a data-saving mode, either in the browser or at the OS level. On Android that's often a deliberate, conscious choice. Treat it as a firm instruction rather than a hint.
// Server side, Node-ish. Adapt to taste.
const wantsLite = req.headers['save-data'] === 'on';
What you must not do is bolt a load of extra logic onto it. No user-agent matching for "Samsung" or "older Android", no deviceMemory < 4 gate that quietly punishes anyone on a cheap handset with a decent connection. Those heuristics age badly and they're borderline discriminatory against people who bought a mid-range phone rather than a flagship.
The Network Information API fills in the gaps, but treat it as advisory:
const conn = navigator.connection;
const slow = conn && (conn.effectiveType === 'slow-2g' || conn.effectiveType === '2g' || conn.effectiveType === '3g');
const saving = conn && conn.saveData;
effectiveType is a rough estimate of the last few round trips, not a promise about the next one. A user on a train can flip from 4g to 3g between the header rendering and the first interaction. Build for that — keep the lite path available at runtime rather than baking the decision in at build time only.
Make the lite build worth having
A lite build that's just your normal bundle with the hero video removed isn't a lite build. It's a diet that consists of not eating the croutons.
What earns its place in the lean version:
- Server-rendered HTML for the first meaningful view, with no client-side routing needed to see the main content.
- Vanilla or near-vanilla interactivity for the two or three things people actually do — search, filter, add to basket, book a slot.
- System font stack instead of a webfont that costs 180KB across four weights. Nobody on a metered connection is going to thank you for Inter at weight 300.
- Images served at the size they're displayed, with
loading="lazy"on everything below the fold and a sensiblesrcset. - No tag manager, no chat widget, no heatmap script. Those are the first things to go and the last things anyone misses.
Kit both builds from the same source. Two entry points, one component library, one set of tests. The moment you fork the codebase into app/ and app-lite/, you've created a maintenance debt that a two-person team will never clear.
The consent question people skip
Here's the awkward bit. Slapping a ?lite=1 query parameter on a URL and then loading a third-party analytics script anyway rather defeats the purpose. If someone has signalled that they're conserving data, respect that in what you load, not just what you render.
In practice that means the lite path should default to no non-essential third parties. Your consent banner still works — it's just that accepting cookies on a lite session should load a lightweight analytics shim rather than the full fat client. If you're using a consent management platform, check whether it supports a reduced configuration; a lot of them do, and almost nobody configures it.
Also worth saying plainly: the lite build is not a way to dodge consent. Same rules, less JavaScript.
Measure the thing you said you'd fix
The temptation is to declare victory because the bundle size dropped from 900KB to 210KB. That's a build statistic, not an outcome. What you want to know is whether real people on real connections had a better time.
A few things worth watching, ideally split by connection type:
- Time to first contentful paint on
3gandslow-2gprofiles, from real users rather than a lab. The Chrome UX Report gives you a decent field baseline, though the UK-level data is broad enough that you'll want your own RUM too. - Bounce rate on the first page view, segmented by
effectiveType. If the lite build isn't shifting it, either the page wasn't the problem or the lite build isn't lite enough. - Task completion for whatever the page is for. A council tax payment, a click-and-collect reservation, a job application. This is the one that matters.
- Bytes transferred per session, which is the number the user actually feels when the SMS from their network arrives on the 28th.
Keep your RUM script tiny — under 2KB gzipped, sent on visibilitychange rather than on a timer, and batched so you're not firing a beacon every time someone scrolls. There's no point measuring data savings with a tool that eats them.
One caveat: don't over-index on effectiveType in your dashboards. It's self-reported by the browser and it lies at the margins. Cross-reference with server-side timings from your own logs where you can.
Roll it out without stranding anyone
Give people an escape hatch. A small, honest link — "switch to the full version" — that sets a cookie and doesn't get overwritten by the next Save-Data header. Some users on a capped plan will still want the map, the video, the 3D product spin. That's their call.
And don't build a lite mode you never look at. Put it in your smoke tests, check it on a real PAYG SIM once a month, and make sure your error tracking covers both entry points. The worst outcome here isn't a slow page — it's a lite page that quietly breaks on a Tuesday and nobody notices until the support desk asks why the booking form has stopped working for half of Cornwall.
Save-Data is a request. Answer it properly and most people won't even notice you did — they'll just find that the site works on the train.