MonkeyLoader All articles
Performance

Save-Data Is a Request, Not a Verdict: Serving Leaner Bundles Without Snobbery

MonkeyLoader
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:

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:

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.

All Articles

Related Articles

The Paper Copy Clause: Loading Print CSS Only When Someone Asks for It

The Paper Copy Clause: Loading Print CSS Only When Someone Asks for It

Dead Weight in the dist Folder: Hunting JavaScript Nobody Loads

Dead Weight in the dist Folder: Hunting JavaScript Nobody Loads

Switch It Off Before They Find It: Five Default Loaders That Fail a UK Accessibility Audit

Switch It Off Before They Find It: Five Default Loaders That Fail a UK Accessibility Audit