India added roughly 80 million internet users last year. Most of them came online through a cheap Android phone on a mobile data connection. When I build websites, I try to keep this person in mind: someone in a tier-3 city, on a Redmi 9A or equivalent, with a data plan that's genuinely fast on a good day and genuinely frustrating on a bad one.
Almost every performance guide I've read assumes a user who is already reasonably well-served by the web. The Lighthouse scores, the Core Web Vitals dashboards, the "you should aim for under 2 seconds" advice — all of this assumes that under 2 seconds is achievable for your actual user on their actual device. For a significant fraction of the people using websites I build, it isn't. Not on the first load. Not over 3G. So you work around that constraint, not with it.
The constraint that actually matters: CPU, not bandwidth
People assume slow phones = slow internet. That's half-true. A cheap Android phone in 2024 often has a surprisingly decent mobile data connection — sometimes 20–30 Mbps on 4G. What it doesn't have is the CPU to parse and execute 400 KB of JavaScript in under a second. The bottleneck is compute, not network.
A Redmi 9A has roughly 1/12th the JavaScript execution speed of a MacBook Pro M2. The same 300 KB React bundle that runs in 0.3 seconds on your dev machine runs in 3.6 seconds on that phone. That's not a 4G problem. That's a JavaScript problem.
This means that techniques like lazy loading images (good for bandwidth) don't help as much as reducing your JavaScript bundle (good for CPU). Switching from a full React SPA to a server-rendered page — even without changing a single other thing — can halve the time to interactive on a cheap phone.
The things that actually work
First: send less JavaScript. This is not glamorous advice. Every framework blog post wants to tell you about code splitting, tree shaking, and micro-frontends. Those all help. But the biggest gains come from asking: do I need this library at all? A date formatting library that weighs 70 KB can be replaced with five lines of Intl.DateTimeFormat. A rich text editor for a contact form is almost certainly overkill.
// Don't import date-fns for this
const fmt = new Intl.DateTimeFormat("en-IN", {
day: "numeric",
month: "short",
year: "numeric",
});
// "10 Sept 2025"
fmt.format(new Date("2025-09-10"));Second: serve images in WebP or AVIF. A JPEG hero image that's 800 KB can be 180 KB in AVIF. That's a real reduction. Use the `<picture>` element and fall back gracefully — Safari finally supports AVIF as of version 16. On mobile, images are often the biggest single contributor to page weight.
<picture>
<source srcset="hero.avif" type="image/avif" />
<source srcset="hero.webp" type="image/webp" />
<img
src="hero.jpg"
alt="Furniture shop interior"
width="1200"
height="630"
loading="lazy"
decoding="async"
/>
</picture>Third: load fonts with font-display: swap and preload your critical font file. Font loading is invisible to most users on fast connections. On 3G it's a visible flash of unstyled text. Preloading the first variant of your main body font costs one extra HTTP request and saves a perceptible visual jank.
What I check before shipping
I run Lighthouse with throttling set to "Slow 4G" on a mid-range mobile device simulation. But I've also started keeping a cheap actual Android phone on my desk — a Redmi 9 that I bought secondhand for ₹3,500. Loading my finished sites on that device tells me things no synthetic benchmark does.
The single most useful number is Total Blocking Time (TBT). If TBT is under 200ms, the page usually feels responsive even on slow hardware. If it's over 600ms, something is executing too much JavaScript on the main thread and users will notice.
Speed is a feature. For most of the people using the websites I build, it's the most important feature. A site that loads in 1.2 seconds on your MacBook but 8 seconds on a Redmi hasn't passed QA.