Designing for slow connections
We design on fast laptops, on office fibre, with the staging environment running locally. Then we ship to a delivery driver checking their rota in a basement car park, or a nurse on a hospital wifi network shared with four hundred other devices.
This gap is well known and still routinely ignored, because the tooling actively hides it. Everything feels instant when you build it. Here is what we have found actually helps, in rough order of how much difference it makes per unit of effort.
Show the last known state immediately
The single largest perceived improvement usually comes from rendering stale data instantly rather than showing a loading state while fresh data arrives. If someone opened the app yesterday and their rota has not changed, waiting two seconds to display identical information is pure loss.
The design work here is mostly about honesty. Stale content needs an unobtrusive indication of its age, and a visible transition when it updates, or people stop trusting what they see. We usually settle on a quiet timestamp plus a brief highlight on anything that changed.
Design the slow state, not just the loading state
Most design files contain a loaded state and a spinner. Real connections produce a third condition: the request has not failed, but it has been going for eight seconds. A spinner communicates nothing useful at that point, and by fifteen seconds it is actively insulting.
We now specify what happens at roughly three seconds, at ten, and at failure. Often the answer at ten seconds is to offer something else to do — let the person queue the action for later, or show them the cached version with a clear note. The worst option is the one most products ship, which is to spin indefinitely and let the user decide when to give up.
Make actions survive the connection dropping
If someone taps "request time off" as they walk into a lift, the request should not evaporate. Queuing writes locally and retrying is an engineering task, but whether it happens at all is usually a design decision, because it only gets built if someone specifies the states around it: pending, retrying, failed, confirmed.
These states need to appear in the design or they will not appear in the product. We have watched this play out often enough to treat it as a rule.
Weigh images honestly
A hero image is a design decision with a bandwidth cost, and the cost is paid by the person with the worst connection, not by the person who chose it. We are not against images. We are against choosing them without knowing what they weigh.
A useful discipline: put the page weight in the design review. Not as a gate, just as a visible number. Decisions change surprisingly often once the number is on screen next to the picture.
Test on the device your users actually have
Every studio says this and few do it consistently. The practical version we have landed on is to keep two deliberately unimpressive Android phones in the studio and require that any significant flow is tried on one of them before sign-off. Not a lab, not a device farm — two cheap phones in a drawer.
Network throttling in browser dev tools is better than nothing, but it models bandwidth well and latency poorly, and latency is usually what hurts. A real device on a real bad connection tells you things the simulator will not.
The underlying point
None of this requires a separate lightweight product, which is the usual proposal and almost always a mistake — two products means one of them is neglected, and it will be the one for the users with less money. It requires treating a poor connection as a normal operating condition rather than an edge case, and designing the states that go with it.