The performance inequality gap
Most people browse on mid-range Android phones and slower networks than yours. Measure the gap, set a byte budget for them, and read the 75th percentile.
- 01Describe the phones people really haveSay which phone and network a typical visitor has, and why your laptop and a new iPhone aren't them.
- 02Estimate the gapTurn CPU and network gaps into times for the same page, and check them against real recordings on three settings.
- 03Set a budgetWork out how many bytes fit in a time target, and read Russell's budgets for JavaScript-light and JavaScript-heavy pages.
- 04Read the 75th percentileWork out a 75th percentile, and say why Core Web Vitals use it, with phones and computers judged apart.
Most people aren't on your laptop
Ravi, who looks after the Leaf and Kettle website, has built a new page that lists the shop's teas. He tries it on his laptop, on the office Wi-Fi. The teas appear in about half a second. Fast enough, he thinks.
Then Asha, from the first lesson, opens the same page on the bus, on her cheap phone. For a few seconds she sees nothing. Then the shop's name appears at the top. The teas come a few seconds after that.
It's the same page and the same code. So why does Ravi wait half a second, and Asha nearly eight? And which of the two should Ravi build for?
The course measured the difference. It loaded the page in Chrome on a fast laptop. Then it loaded it on the same laptop, slowed down on purpose to act like a low-end phone on a weak 3G network. Here is when the teas appeared each time:
// When the teas appeared, in ms, in two loads of the same pageconst fastLaptopMs = 564;const lowEndPhoneMs = 7628; const times = lowEndPhoneMs / fastLaptopMs;console.log(`The low-end phone waited ${times.toFixed(1)} times as long.`);- Line 1 is a comment. Times are in milliseconds (ms), thousandths of a second, from the start of the visit.
- Lines 2 and 3 hold the two recorded times: 564 ms on the fast laptop, and 7,628 ms on the low-end phone.
- Line 5 divides the slow time by the fast one.
- Line 6 prints
The low-end phone waited 13.5 times as long.
Thirteen and a half times as long, for the same page. Ravi's laptop was the wrong place to judge it. Here are both loads in one picture. In the middle is a third load: the same laptop, slowed down much the way Lighthouse, the test tool from lesson 1.1, tests a phone.
app.js, to arrive. The shaded bar is the phone running it, as a long task. The time at the end of each row is when the teas appeared.The difference between Ravi's laptop and Asha's phone has a name. Alex Russell, who writes about web performance, calls it the performance inequality gap. It's the gap between the fast devices and networks developers use, and the slower ones most people have. Russell has tracked it in a series of articles since 2017. This lesson uses his 2026 edition.
A chef writes a recipe in a restaurant kitchen, with a big oven and six burners. It's ready in 20 minutes, so that's what the card says. But most people cook it at home, on a small hob with two burners. Same recipe, same steps, and it takes them far longer than the card promised.
- In real life: The recipe card
- In web performance: The page: its HTML, CSS and JavaScript
- In real life: The restaurant kitchen with its big oven
- In web performance: Ravi's fast laptop on the office Wi-Fi
- In real life: "Ready in 20 minutes" on the card
- In web performance: The load time Ravi measured on his laptop
- In real life: A small hob in a home kitchen
- In web performance: Asha's cheap phone on a weak signal
- In real life: The home cook's real wait
- In web performance: The visitor's real load time
The performance inequality gap is the difference between the fast devices and networks developers test on and the slower ones most visitors have. So don't judge a page on your own laptop. Judge it on the phone and network of a typical visitor.
Which visitor, exactly? Lesson 1.2 met the 75th percentile: line everything up from best to worst, and go three quarters of the way along. Russell lines up visitors by how fast their phone and network are. The one three quarters along is the one he tells you to test with. A full quarter of visitors have something slower still.
In his 2026 edition, that visitor has:
- a phone like the Samsung Galaxy A24 4G, an Android from 2023;
- a network that downloads 9 megabits a second, uploads 3, and takes 100 ms for a round trip.
The next three sections take that visitor apart: the phone, its chip, and its network.
What the median phone really is
Start with the phone in the visitor's hand. Here is what Russell's sales figures show:
- In none of the past twelve years were iPhones more than 20% of the new phones sold.
- Cheap phones outsell expensive ones by more than 3 to 1.
- Some low-end phones now cost less than $100.
- People keep their phones longer than before. The median phone is now kept for more than two years.
- Phones at the 75th percentile may be nearly three years old.
Russell sums it up: most phones in use are Androids more than two years old. So the median phone, the middle one if you line them all up, isn't a new iPhone. The phone at the 75th percentile is slower still.
Russell's test phone, the Galaxy A24 4G, came out in 2023 for $250, when the average phone sold for $353. It may be hard to buy now, so any phone with the same chip will do, such as the Galaxy A16 4G.
Russell follows four groups of phones. A phone's CPU, the chip that runs the code, has several cores: workers that can each run one thing at a time. The table compares one core of each phone, because a page's JavaScript mostly depends on one. Read each row across:
| Group | Price when new | Speed of one CPU core |
|---|---|---|
| Fastest iPhone | About $1,000 and up. | The fastest phones there are, used as the yardstick here. |
| Fastest Android | Russell doesn't give one. | Closing the gap with the fastest iPhones, but sold in far smaller numbers than cheaper Androids. |
| Mid-range Android | $300 to $350. | More than 3.5 times slower than a recent iPhone. |
| Low-end Android | $100 to $150, some now under $100. | 9 times slower than a recent iPhone, and not much faster than in 2022. |
What your analytics can and can't show
Your analytics may show far more than 20% iPhones. Wikimedia, which runs Wikipedia, sees more than 40% of its phone visits come from iPhones. Russell gives two reasons:
- People with more money, who can afford iPhones, show up more in these statistics.
- Used and refurbished iPhones are a big and growing market. So the recent iPhone a developer carries isn't what most iPhone owners have.
So check your own numbers, but know what they can't show. People who gave up on a slow site don't appear in its usage numbers at all. Russell describes a fast-growing site that added nearly 100 KiB of JavaScript to what its pages loaded first, every month for six months. A KiB is 1,024 bytes. Each month, a smaller share of its visits came from phones. Then the team removed code nobody used, and files fetched too early. The phone visitors came back.
A shop has three steep steps up to its door. The owner counts her customers each day, and they all walk in easily. She never sees the people with prams, wheelchairs or heavy bags who looked at the steps and went elsewhere. A ramp lets them in too, and it's easier for everyone else as well.
- In real life: The steep steps up to the door
- In web performance: A heavy page that needs a fast phone and network
- In real life: People who look at the steps and leave
- In web performance: Visitors who give up before the page loads
- In real life: The owner's count of customers inside
- In web performance: Your analytics: only the visits that got in
- In real life: A ramp that everyone can use
- In web performance: A page that's fast on a cheap phone and a slow network
So speed isn't a finishing touch for people with fast phones. It decides who can use your site at all.
Computers are part of the picture too
Laptops and desktops are only 15 to 18% of the devices sold that can browse the web. And not all of them are fast. About 80% run on batteries, so to save power and stay cool, their chips often run below full speed. Microsoft Edge sorts the computers it runs on into three tiers. Read each row across:
| Tier | What puts a computer in it | Share of computers |
|---|---|---|
| Low-end | 4 CPU cores or fewer, or 4 GB of memory or less. | 30% |
| Medium | 4 to 8 cores, or 4 to 16 GB of memory, or a spinning hard disk. | 60% |
| High-end | More than 8 cores, more than 16 GB of memory, and a solid-state disk. | 10% |
Nearly a third are low-end, and only 10% are high-end.
CPU speed: 3.5 to 9 times slower
Remember the one person behind the counter from lesson 1.1? That's the page's main thread. It runs the page's JavaScript one task at a time, and a tap waits until the current task is done. The main thread runs on one core at a time. So for JavaScript, how fast one core is matters more than how many there are.
Russell finds that the speed of JavaScript-built pages closely follows the speed of one core. The cheapest phones have had eight cores since 2019, but eight slow cores don't make the main thread faster.
A road climbs a steep hill to a village. A sports car is at the top in a minute. A bicycle loaded with shopping takes a quarter of an hour on the same road. The hill didn't change, only the engine. And a longer hill adds a little time for the car, but a lot for the bicycle.
- In real life: The hill
- In web performance: The page's JavaScript: the same code on every phone
- In real life: The sports car
- In web performance: A recent iPhone's fast CPU core
- In real life: A bicycle loaded with shopping
- In web performance: A low-end Android's slow core
- In real life: The time to reach the top
- In web performance: How long the main thread stays busy
- In real life: A longer hill
- In web performance: More JavaScript: a little more time on the iPhone, a lot more on the cheap phone
Russell compares phones with Geekbench, a test that scores how fast a CPU works. On one core, a mid-range Android is more than 3.5 times slower than a recent iPhone. A low-end Android is 9 times slower. Say a tap runs 40 ms of JavaScript on a recent iPhone:
const iphoneMs = 40;const phones = [ { name: "a recent iPhone", slower: 1 }, { name: "a mid-range Android", slower: 3.5 }, { name: "a low-end Android", slower: 9 },]; for (const phone of phones) { const ms = iphoneMs * phone.slower; console.log(`${phone.name}: ${ms} ms${ms > 50 ? ", a long task" : ""}`);}- Line 1 is the work's time on a recent iPhone: 40 ms.
- Lines 2 to 6 list three phones, each with how many times slower its core is.
- Line 9 multiplies the iPhone's time by each phone's slowdown.
- Line 10 prints the time. When it's over 50 ms, it adds
a long task. - It prints
a recent iPhone: 40 ms,a mid-range Android: 140 ms, a long taskanda low-end Android: 360 ms, a long task.
A long task, from lesson 1.1, is the main thread busy for more than 50 ms in a row. A tap during it waits. So the same code makes no long task on the iPhone, and makes Asha wait over a third of a second.
Why it matters: every bit of JavaScript you add costs the slowest phones the most. And the gap isn't closing on its own. Russell finds that the chips in budget phones haven't really got faster since 2022.
Time some real work on your own device. The panel does the same start-up work as the shop page later in this lesson:
const start = performance.now();buildShop();const ms = performance.now() - start; showTime("this device", ms);showTime("a mid-range Android, at least", ms * 3.5);showTime("a low-end Android", ms * 9);Not timed yet.
Press Time it. The panel does the shop's start-up work 5 times on this device and keeps the middle time.
- Line 2 of the panel's code builds a catalog of 1,800 teas, sorts 200,000 prices, and builds 24 cards.
- Lines 1 and 3 time it with
performance.now(), the milliseconds since the page started loading. - Lines 6 and 7 multiply your time by 3.5 and by 9. They're estimates that treat your device as a recent iPhone.
The panel's heading also shows two numbers the browser reports:
navigator.hardwareConcurrency: the number of logical processors, or how many threads the CPU can run at once.navigator.deviceMemory: about how many gigabytes of memory the device has. Only Chromium browsers report it, and they round it so sites can't use it to recognize you.
You can't carry every phone around. So, as lesson 1.1 showed, tools slow your computer down on purpose: that's throttling. Lighthouse makes the CPU 4 times slower for its phone test. Since Chrome 134, DevTools can measure your computer and offer CPU settings matched to a low-tier and a mid-tier phone. Lesson 8.3, Throttling and simulation, shows how both work.
How big is the gap on real sites? The Web Almanac 2025, HTTP Archive's yearly study of the web, tested pages on a desktop setting and a phone setting. It measured Total Blocking Time: how long, in total, long tasks kept the main thread from answering during a load. For the median page, that was 92 ms on desktop, and 1,916 ms on the phone: nearly two seconds.
Network speeds around the world
The bytes still have to reach the phone. Here the gap is about money as well as speed.
A shop posts the same parcel to two customers. One lives in the city. Her delivery is cheap, and the parcel arrives the next day. The other lives in a village far away. Her delivery costs more, and the parcel takes a week. The parcel is the same. The road and the price aren't.
- In real life: The parcel
- In web performance: The page's bytes: the same for every visitor
- In real life: The delivery charge
- In web performance: What the bytes cost on the visitor's data plan
- In real life: The slow road to a far village
- In web performance: A slow network: a low download speed and long round trips
- In real life: Packing a smaller parcel
- In web performance: Sending fewer bytes: cheaper and quicker for everyone
Start with the price. In lesson 1.2, a data plan was a taxi meter: every byte counts against it. The International Telecommunication Union (ITU), a United Nations agency, compares the same kind of mobile data plan in every country. People in low-income countries spend about 22 times as large a share of their income on it as people in high-income countries.
Then the road. Russell takes his network numbers from Cloudflare Radar, Cloudflare's public report on internet speed and quality around the world. At the 75th percentile, a connection:
- downloads 9 megabits a second (Mbps);
- uploads 3 Mbps;
- takes 100 ms for a round trip: a message to a server and its answer back.
A megabit is a million bits, and a byte is 8 bits, so 9 Mbps brings about 1.1 million bytes a second. Each row of this table is one network you'll meet in this course:
| Setting | Download | Upload | Round trip | Where it comes from |
|---|---|---|---|---|
| The 75th percentile connection | 9 Mbps | 3 Mbps | 100 ms | Alex Russell, from Cloudflare Radar |
| The median connection | About 17 Mbps | Not given | Not given | Alex Russell, from Cloudflare Radar |
| Lighthouse's phone test | 1.6 Mbps | 0.75 Mbps | 150 ms | The Lighthouse docs |
| DevTools' Slow 4G | 1.44 Mbps | 0.675 Mbps | 150 ms (it adds 562.5 ms) | The DevTools source code |
| DevTools' 3G | 0.4 Mbps | 0.4 Mbps | 400 ms (it adds 2,000 ms) | The DevTools source code |
Two findings stand out:
- The gap is growing. Since early 2024, the slowest quarter of connections improved from about 7 to 9 Mbps. Over the same time, the gap between the slower quarter and the faster quarter grew from 15 to 21 Mbps.
- 5G isn't everywhere. 5G covered just over half the world in 2025, says the GSMA report Russell quotes. And many people need a newer, more expensive phone to use it. Russell puts it this way: "4G is a miracle, and 5G is a mirage."
What do these speeds mean for a page? Take the median mobile page, which Russell puts at 2.6 MiB, and download it on three networks:
const networks = [ { name: "a fast office line", mbps: 100, rttMs: 10 }, { name: "the 75th percentile", mbps: 9, rttMs: 100 }, { name: "DevTools' Slow 4G", mbps: 1.44, rttMs: 150 },];const bytes = 2.6 * 1024 * 1024; for (const network of networks) { const ms = network.rttMs * 4 + (bytes * 8) / (network.mbps * 1000); console.log(`${network.name}: about ${Math.round(ms / 100) / 10} s`);}- Lines 1 to 5 are three networks: a made-up fast office line for comparison, the 75th percentile, and DevTools' Slow 4G.
- Line 6 is the page's size in bytes. A MiB (mebibyte) is 1,024 × 1,024 bytes, a little more than a million.
- Line 9 is a simple model. It spends 4 round trips to find the server, connect, make the line private and ask for the page. Then the bytes come at full speed: bytes × 8 gives bits, and 1 Mbps carries 1,000 bits each millisecond.
- Line 10 prints
a fast office line: about 0.3 s,the 75th percentile: about 2.8 sandDevTools' Slow 4G: about 15.7 s.
Same page: under half a second, or nearly sixteen. Why it matters: on a slow road, every byte you don't send saves the visitor time and money. Real loads are messier, because new connections start slowly. Lesson 4.1, Latency vs bandwidth, and lesson 4.3, TCP connections and slow start, cover that. The system design lesson Back-of-the-envelope numbers for frontend practises estimates like this.
One page, three experiences
Back to Ravi's tea page. Which part of the gap made Asha wait? The page's content is built by JavaScript, as on many sites. Its HTML holds only a header and an empty <main>:
<!doctype html><html lang="en"><head> <meta charset="utf-8"> <meta name="viewport" content="width=device-width, initial-scale=1"> <title>Leaf and Kettle</title> <link rel="stylesheet" href="styles.css"> <script src="app.js" defer></script></head><body> <header>Leaf and Kettle</header> <main id="app"></main></body></html>- Line 8 loads
app.js. Itsdeferattribute tells the browser to run it once the HTML has been read. - Line 12 is the empty
<main>thatapp.jsfills in.
Here is app.js, 153 KB in all, shortened:
const catalog = [ { name: "Mint No. 1", garden: "Assam", notes: "bright and fresh", cents: 250 }, { name: "Green No. 1", garden: "Nilgiri", notes: "citrus and honey", cents: 1169 }, // … 1,798 more teas …]; // Start-up work, like building a search index: sort 200,000 past order prices.const prices = Array.from({ length: 200000 }, (_, i) => (i * 7919) % 1000);prices.sort((a, b) => a - b); // Sort by price, then show the intro and the cheapest 24 teas.const sorted = [...catalog].sort((a, b) => a.cents - b.cents);const gardens = new Set(catalog.map((tea) => tea.garden));const cards = sorted.slice(0, 24).map((tea) => `<article><h2>${tea.name}</h2><p>${tea.garden}: ${tea.notes}. ${tea.cents} cents.</p></article>`); document.querySelector("#app").innerHTML = `<h1>Our teas</h1><p id="intro">${catalog.length} teas from ${gardens.size} gardens, from ${sorted[0].cents} cents. Every tea is packed the week you order it and sent in a paper bag you can compost.</p>${cards.join("\n")}`;- Lines 1 to 5 are the catalog: 1,800 teas, written out in the file. They make up most of its size.
- Lines 8 and 9 stand in for the start-up work a real shop does, such as building a search index. They sort 200,000 prices.
- Lines 12 to 14 sort the teas by price and build cards for the 24 cheapest.
- Lines 16 to 18 put a heading, an introduction and the cards into the page. Nothing useful shows until they run.
The course loaded this page in real Chrome 156, on a phone-sized screen, 5 times on each of three settings. The CPU slowdowns are compared with the recording computer, an Apple M1 Pro laptop:
- A fast computer: Full CPU speed, DevTools' Fast 4G.
- Lighthouse's phone: CPU 4 times slower, DevTools' Slow 4G.
- A low-end phone: CPU 9 times slower, DevTools' 3G.
The table gives the times behind the picture at the top of the lesson. Each row is one setting, from the load with the middle LCP of its 5. LCP, from lesson 1.1, is when the largest text or image appeared: here, the shop's introduction.
| Setting | CPU and network | app.js arrived | Long task | Main content (LCP) |
|---|---|---|---|---|
| A fast computer | Full CPU speed, DevTools' Fast 4G | 507 ms | None | 564 ms, good |
| Lighthouse's phone | CPU 4 times slower, DevTools' Slow 4G | 2,035 ms | 155 ms | 2,212 ms, good |
| A low-end phone | CPU 9 times slower, DevTools' 3G | 7,232 ms | 366 ms | 7,628 ms, poor |
So what made the low-end load 13.5 times as long? Subtract the fast laptop's times from the low-end ones:
// Two loads of the same page, in msconst fastLaptop = { scriptArrived: 507, longTask: 0 };const lowEndPhone = { scriptArrived: 7232, longTask: 366 }; const extraWait = lowEndPhone.scriptArrived - fastLaptop.scriptArrived;const extraWork = lowEndPhone.longTask - fastLaptop.longTask;console.log(`Extra time waiting for app.js: ${extraWait} ms`);console.log(`Extra time in long tasks: ${extraWork} ms`);- Line 2 is the fast laptop.
app.jsarrived at 507 ms, and running it made no long task. - Line 3 is the low-end setting.
app.jsarrived at 7,232 ms, and running it was a 366 ms long task. - Lines 5 and 6 subtract, to find how much longer each part took.
- Lines 7 and 8 print
Extra time waiting for app.js: 6725 msandExtra time in long tasks: 366 ms.
The teas came 7,064 ms later on the low-end setting. Waiting for app.js to come over the slow network was 6,725 ms of that. The slow CPU added a long task on top.
Why it matters: on this page, the network made the biggest difference, so sending fewer bytes is the first thing to try. The CPU still matters. The same script made no long task on the laptop, a 155 ms one with Lighthouse's settings, and a 366 ms one on the low-end setting.
The filmstrips show what a visitor saw: the header first, then a wait, then the teas.
- Nothing drawn yet
0 ms to 366 msA blank screen

At 366 msThe header only

At 565 msThe teas
- Nothing drawn yet
0 ms to 1,203 msA blank screen

At 1,203 msThe header only

At 2,212 msThe teas
- Nothing drawn yet
0 ms to 4,203 msA blank screen

At 4,203 msThe header only

At 7,629 msThe teas
You can open the shop and measure it yourself. Or download the traces for the fast, Lighthouse and low-end loads, and open them in DevTools' Performance panel.
A realistic budget for bytes and JavaScript
Ravi now knows the page is too slow for Asha. So how big may it be? He needs a performance budget: a limit set in advance, like deciding before a holiday how much you'll spend. It can limit how many bytes a page sends, or how long it takes to load.
Start from a time target and the 75th percentile network. How many bytes fit in 3 seconds?
const rttMs = 100;const mbps = 9;const targetMs = 3000; const setupMs = 4 * rttMs;const bytesPerMs = (mbps * 1000000) / 8 / 1000;const budgetBytes = (targetMs - setupMs) * bytesPerMs; console.log("time left for bytes:", targetMs - setupMs, "ms");console.log("budget:", (budgetBytes / 1024 / 1024).toFixed(1), "MiB");- Lines 1 to 3 are the 75th percentile network, 100 ms and 9 Mbps, and a target of 3 seconds.
- Line 5 spends 4 round trips before any bytes arrive, as in the download example: 400 ms.
- Line 6 turns 9 Mbps into bytes each millisecond: 1,125.
- Line 7 fills the rest of the time with bytes.
- Lines 9 and 10 print
time left for bytes: 2600 msandbudget: 2.8 MiB.
2.8 MiB is an upper limit. Russell's budget calculator counts more than this simple model, so its budgets are smaller:
- Connections. His budgets assume the page opens two connections, and each one takes time to set up.
- The phone's CPU time. JavaScript costs the most: after it downloads, the slow CPU still has to read it and run it.
Most bytes are like a sandwich: once you've carried it home, you can eat it. JavaScript is like a meal kit: you still have to cook it, and on a slow phone the cooking takes longest.
So the calculator sorts pages into two kinds:
- JavaScript-light pages, where 15% of the bytes are JavaScript;
- JavaScript-heavy pages, where half of them are.
Each row of the table is one time target, with the budget for each kind of page:
| Time target | JavaScript-light page | JavaScript-heavy page |
|---|---|---|
| 3 seconds | 2.0 MiB, of which 0.3 MiB JavaScript | 1.2 MiB, of which 0.62 MiB JavaScript |
| 5 seconds | 3.7 MiB, of which 0.57 MiB JavaScript | 2.3 MiB, of which 1.15 MiB JavaScript |
A page that opens four connections instead of two loses another 350 KiB from the 3-second budget.
Now compare them with the web today, from the HTTP Archive figures Russell quotes:
- The median mobile page is 2.6 MiB. That's already over the 3-second budget for a JavaScript-light page, 2.0 MiB.
- On mobile, the median page sends 680 KiB of JavaScript. That's more than either 3-second budget allows for JavaScript: 0.3 MiB, or 0.62 MiB, which is about 635 KiB.
- The page at the 75th percentile sends 1.3 MiB of JavaScript.
Why it matters: at least half of mobile pages are already bigger than a 3-second budget allows. Lesson 6.4, The cost of JavaScript, shows where JavaScript's CPU time goes.
Why the 75th percentile, not the average
Ravi has made his page smaller. Is it fast enough for his real visitors now? Every visit has its own time: most are fast, some slow, a few very slow. How do you sum that up in one number? A bus company has the same problem:
A bus company wants to tell riders how late its buses run. The average is no good: one bus stuck in a flood for an hour makes the whole week look bad. So it promises: "3 of every 4 buses arrive within 5 minutes of the timetable." And it checks that promise on each route on its own. Otherwise the quick city buses would hide the late village ones.
- In real life: Each bus's arrival
- In web performance: Each page load's time
- In real life: "3 of every 4 buses arrive within 5 minutes of the timetable"
- In web performance: The 75th percentile: 3 of every 4 loads take this long or less
- In real life: One bus stuck for an hour, which drags the average up
- In web performance: A few very slow loads pull the average up
- In real life: City routes and village routes, checked apart
- In web performance: Phones and computers, judged apart
Page loads work the same way. To find the 75th percentile, line the loads up from fastest to slowest and go three quarters of the way along. Here it is for ten page loads:
const lcps = [1200, 1400, 1500, 1700, 1900, 2100, 2600, 3400, 5200, 9800];const sorted = [...lcps].sort((a, b) => a - b); const place = Math.ceil(0.75 * sorted.length);console.log(`75th percentile: load ${place} of ${sorted.length}, ${sorted[place - 1]} ms`);- Line 1 holds ten LCP times, one per page load.
- Line 2 copies them and sorts them, fastest first.
- Line 4 works out the place three quarters along. 0.75 × 10 is 7.5, and
Math.ceilrounds it up to 8. - Line 5 prints
75th percentile: load 8 of 10, 3400 ms. An array counts from 0, so load 8 issorted[7].
So 8 of the 10 loads took 3,400 ms or less. Rounding up to a real load like this is the nearest-rank method.
Now take the average of the same ten loads:
const lcps = [1200, 1400, 1500, 1700, 1900, 2100, 2600, 3400, 5200, 9800];const average = lcps.reduce((sum, ms) => sum + ms, 0) / lcps.length;const faster = lcps.filter((ms) => ms < average).length; console.log(`average: ${average} ms`);console.log(`loads faster than the average: ${faster} of ${lcps.length}`);- Line 2 adds up all ten times, starting from 0, and divides by 10.
- Line 3 counts the loads that were faster than the average.
- Lines 5 and 6 print
average: 3080 msandloads faster than the average: 7 of 10.
The average is slower than 7 of the 10 loads. Two very slow loads, 5,200 and 9,800 ms, pulled it up, like the bus stuck in the flood. The median, the 5th load, says something else again: 1,900 ms. It looks fine, because it ignores the slower half.
web.dev, Google's guide to web performance, recommends the 75th percentile of page loads, "segmented across mobile and desktop devices." So phones and computers are judged apart, like the city and village buses. Try it on 1,000 made-up loads:
const sorted = [...loads].sort((a, b) => a - b);
const at = (p) => sorted[Math.ceil((p / 100) * sorted.length) - 1];
showLine("average", loads.reduce((sum, ms) => sum + ms, 0) / loads.length);
showLine("median", at(50));
showLine("75th percentile", at(75));
showLine("95th percentile", at(95));- Average
- 1,972 ms
- Median (50th percentile)
- 1,605 ms
- 75th percentile
- 2,490 ms
- 95th percentile
- 4,520 ms
All loads: 1,000 loads. The 75th percentile is 2,490 ms, so 3 of every 4 loads took that long or less, within the 2,500 ms limit. The average, 1,972 ms, sits above the median, 1,605 ms, because a few very slow loads pull it up.
All the loads together have a 75th percentile of 2,490 ms. That's just inside 2.5 seconds, the limit for a good LCP. The phones alone have 2,907 ms: over the limit. Mixed together, the fast computers hid a problem for at least a quarter of the phone loads. That's why Google's tools report phones and computers apart.
Lesson 3.4, Averages lie: reading distributions, goes deeper into these numbers. Lesson 12.5, Segmenting field data, splits real visits into groups.
Benchmarks: the Web Almanac and CrUX
How does the rest of the web do on phones? The Chrome UX Report (CrUX), from lesson 1.2, is Google's public dataset of Core Web Vitals from real Chrome users' visits. The Web Almanac reads it for millions of sites. Lesson 1.2 quoted its latest result: 48% of sites had good Core Web Vitals on phones, and 56% on computers. Each row of this table is one year:
| Year | Phones | Computers |
|---|---|---|
| 2021 | 32% | 41% |
| 2022 | 31% | 44% |
| 2023 | 36% | 48% |
| 2024 | 44% | 55% |
| 2025 | 48% | 56% |
Three findings:
- Phones still trail computers in every year, and most sites still don't pass on phones.
- Popular sites aren't always faster. On phones, 51% of the 1,000 most popular sites passed, but only 37% of the top 100,000.
- Pages after the first do better. On phones, 56% of secondary pages passed, against 45% of home pages. The Web Almanac suggests one reason: files the browser saved on an earlier page can be used again.
Why it matters: if your site passes on phones, it's ahead of about half the web. To see your own site's CrUX numbers, put its address into PageSpeed Insights. CrUX is field data: timings from real visits, like the till in lesson 1.1. Lesson 3.3, Lab data vs field data, compares it with lab tests. Lesson 10.1, The Chrome User Experience Report, covers CrUX in depth. CrUX only counts Chrome users, so lesson 12.2, Field data from every browser, collects your own.
Choose a test setup
Ravi can't test on every phone. But he can test on something close to the 75th percentile visitor: slower than his laptop, but not the slowest there is. Sort each setup by where it falls:
- A new laptop on office Wi-Fi
- A new iPhone on a fast connection
- DevTools on your laptop with Fast 4G and no CPU slowdown
- A Samsung Galaxy A24 4G on 9 Mbps with a 100 ms round trip
- A Samsung Galaxy A16 4G on the same 9 Mbps, 100 ms network
- An HP 14 laptop with a Celeron N4500 on Wi-Fi
- A Moto E phone on DevTools' 3G
- A $100 Android on a 0.4 Mbps connection
Sort each test setup: faster than the 75th percentile, close to it, or slower.
In practice, a setup close to the 75th percentile looks like this:
- A real phone in the Galaxy A24 class, if you can get one. Nothing beats feeling the page in your hand.
- In DevTools, the CPU set to the matched mid-tier phone, and a custom network of 9 Mbps down, 3 Mbps up and a 100 ms round trip.
- Your own visitors' 75th percentile on phones, from CrUX or your own monitoring, to check your guess.
If you build for places where phones and networks are slower, Russell suggests aiming at the 90th or 95th percentile instead. Lesson 9.3, Choosing test conditions, builds a setup step by step.
Common mix-ups
- "Phones are as fast as laptops now." No. The fastest are. But a low-end Android's core is 9 times slower than a recent iPhone's, and cheap phones outsell expensive ones by more than 3 to 1.
- "5G has made networks fast." No. It covers only about half the world, and it needs a newer phone. The 75th percentile connection is 9 Mbps, with a 100 ms round trip.
- "Lighthouse's phone test shows what my slowest visitors get." No. It's one setting, with the CPU only 4 times slower than the computer running it. In the recordings, the low-end setting took 3.4 times as long as Lighthouse's settings.
- "The average load time describes my visitors." No. A few very slow loads pull it up, so it can be slower than most loads. Use percentiles.
- "If all visits together pass, phones pass." No. Fast computers can pull the combined number down. Check phones and computers apart.
- "My analytics show mostly iPhones, so slow phones don't matter." No. Many iPhones in use are old ones. And visitors who gave up on a slow page don't show up in your numbers at all.
Three numbers that describe a set of loads are easy to mix up. Read each row across: the number, what it tells you, and what it hides:
| Number | What it tells you | What it hides |
|---|---|---|
| The average | The total divided by the number of loads. | A few very slow loads pull it up, so it can describe nobody's visit. |
| The median (50th percentile) | The middle load: half were faster, half slower. | It says nothing about the slower half, where the problems are. |
| The 75th percentile | 3 of every 4 loads were this fast or faster. | The slowest quarter. Look at the 95th percentile, or at groups, to see them. |
Practice exercises
Answer first, then use the hints.
Ravi makes the work smaller: it now takes 12 ms on a recent iPhone. Which phone still gets a long task?
const iphoneMs = 12;
const phones = [
{ name: "a recent iPhone", slower: 1 },
{ name: "a mid-range Android", slower: 3.5 },
{ name: "a low-end Android", slower: 9 },
];
for (const phone of phones) {
const ms = iphoneMs * phone.slower;
console.log(`${phone.name}: ${ms} ms${ms > 50 ? ", a long task" : ""}`);
}Only the low-end Android. 12 × 9 is 108 ms, over 50. The mid-range Android takes 42 ms, under the limit. Smaller work helps every phone, but the slowest one needs it most.
What does this print?
const lcps = [900, 1100, 1200, 1300, 1600, 2000, 2800, 6000];
const sorted = [...lcps].sort((a, b) => a - b);
const place = Math.ceil(0.75 * sorted.length);
console.log(sorted[place - 1]);2000. 0.75 × 8 is exactly 6, so Math.ceil leaves it at 6, and the 6th load is 2,000 ms. 6 of the 8 loads took 2,000 ms or less.
This is the budget example with a 5-second target. What budget does it print?
const rttMs = 100;
const mbps = 9;
const targetMs = 5000;
const setupMs = 4 * rttMs;
const bytesPerMs = (mbps * 1000000) / 8 / 1000;
const budgetBytes = (targetMs - setupMs) * bytesPerMs;
console.log("time left for bytes:", targetMs - setupMs, "ms");
console.log("budget:", (budgetBytes / 1024 / 1024).toFixed(1), "MiB");It prints budget: 4.9 MiB. 4,600 ms × 1,125 bytes is 5,175,000 bytes, about 4.9 MiB. Russell's 5-second budgets are lower, 3.7 MiB and 2.3 MiB, because his calculator also counts setting up two connections and the phone's CPU time.
This report says the page is good, though three of its loads are slow. Fix it to use the 75th percentile. What does it print then?
const lcps = [700, 800, 900, 1000, 1100, 2700, 3100, 3700];
function rate(lcps) {
const typical = lcps.reduce((sum, ms) => sum + ms, 0) / lcps.length;
return typical <= 2500 ? "good" : "not good";
}
console.log(rate(lcps));const lcps = [700, 800, 900, 1000, 1100, 2700, 3100, 3700];
function rate(lcps) {
const sorted = [...lcps].sort((a, b) => a - b);
const p75 = sorted[Math.ceil(0.75 * sorted.length) - 1];
return p75 <= 2500 ? "good" : "not good";
}
console.log(rate(lcps));With the 75th percentile, it prints not good. The average is 1,750 ms, so the old report said good. But 3 of the 8 loads took 2,700 ms or more. The 75th percentile is the 6th load, 2,700 ms: over the limit.
All these visits together pass the 2,500 ms limit at the 75th percentile. Which group fails on its own?
const mobile = [1900, 2300, 2600, 3200];
const desktop = [700, 800, 900, 1000, 1100, 1200, 1300, 1400];
const p75 = (values) => {
const sorted = [...values].sort((a, b) => a - b);
return sorted[Math.ceil(0.75 * sorted.length) - 1];
};
console.log("all visits:", p75([...mobile, ...desktop]));
console.log("mobile:", p75(mobile));
console.log("desktop:", p75(desktop));Mobile. All visits together have a 75th percentile of 1,900 ms, a pass, because the 8 fast computer visits outnumber the 4 phone visits. The phones alone have 2,600 ms: over the limit. That's why Core Web Vitals are judged for phones and computers apart.
Open the shop page in Chrome and open DevTools. In the Performance panel, set the CPU to 4 times slower and record a reload. Or download the trace recorded with Lighthouse's settings, and load it in the panel. Find the long task in the Main track. What does DevTools call the work inside it?
Evaluate script, for app.js. It's the browser running the whole file: the catalog, the price sort and the cards. On your own computer the bar may be shorter, but it's the same step.
Check your understanding
For each question, ask who is in the slowest quarter of visits, and what their CPU and network do to the page.
Question 1 of 7What does the last line of the three-phones example print?
Read the code, then predictconst iphoneMs = 40; const phones = [ { name: "a recent iPhone", slower: 1 }, { name: "a mid-range Android", slower: 3.5 }, { name: "a low-end Android", slower: 9 }, ]; for (const phone of phones) { const ms = iphoneMs * phone.slower; console.log(`${phone.name}: ${ms} ms${ms > 50 ? ", a long task" : ""}`); }Choose an answer to see the explanation.
Question 2 of 7Alex Russell's test phone is a Samsung Galaxy A24 4G, a phone from 2023. Why that phone?
Choose an answer to see the explanation.
Question 3 of 7Your analytics say most of your visitors use iPhones. Can you skip testing on slow phones?
Choose an answer to see the explanation.
Question 4 of 7What does the 75th percentile example print?
Read the code, then predictconst lcps = [1200, 1400, 1500, 1700, 1900, 2100, 2600, 3400, 5200, 9800]; const sorted = [...lcps].sort((a, b) => a - b); const place = Math.ceil(0.75 * sorted.length); console.log(`75th percentile: load ${place} of ${sorted.length}, ${sorted[place - 1]} ms`);Choose an answer to see the explanation.
Question 5 of 7Why does web.dev judge Core Web Vitals at the 75th percentile, with phones and computers apart?
Choose an answer to see the explanation.
Question 6 of 7In the three recorded loads of the same shop, what made the low-end load's main content come so much later?
Choose an answer to see the explanation.
Question 7 of 7Why is Alex Russell's budget for a JavaScript-heavy page smaller than for a JavaScript-light page?
Choose an answer to see the explanation.
Key takeaways
- The performance inequality gap is the difference between the devices and networks developers test on and the slower ones most people have.
- Most phones in use are Androids more than two years old. Russell's 75th percentile test phone is a Samsung Galaxy A24 4G.
- On one CPU core, a low-end Android is 9 times slower than a recent iPhone, and a mid-range one more than 3.5 times. The same JavaScript costs the slowest phones the most.
- The 75th percentile connection downloads 9 Mbps, uploads 3 Mbps, and takes 100 ms for a round trip. The same bytes cost some people far more than others.
- The same shop page showed its teas in 564 ms on a fast laptop and 7,628 ms on a low-end setting. Most of the difference was waiting for the network.
- At the 75th percentile, a 3-second page can afford about 2.0 MiB if it's light on JavaScript, and 1.2 MiB if half of it is JavaScript.
- Judge speed at the 75th percentile, with phones and computers apart. The average hides the slow visits, and your analytics miss the people who gave up.
Remember the one-liner.
Build for the phone and network that a quarter of your visitors are slower than, not for the laptop on your desk.
Sources
The facts on this page come from these articles, datasets and docs:
- The Performance Inequality Gap, 2026, Alex Russell.
- Network throttling, the Lighthouse docs.
- NetworkManager.ts, where DevTools defines its network presets, Chrome DevTools source code.
- What's new in DevTools, Chrome 134 (calibrated CPU throttling), Chrome for Developers.
- Facts and Figures 2025: Affordability of ICT services, International Telecommunication Union (ITU).
- Cloudflare Radar, internet quality, Cloudflare.
- Web Vitals, web.dev.
- Defining the Core Web Vitals metrics thresholds, web.dev.
- Performance, Web Almanac 2025, HTTP Archive.
- Chrome UX Report (CrUX), Chrome for Developers.
- Navigator: hardwareConcurrency and deviceMemory, MDN.
- High Resolution Time (performance.now), W3C.
Coming next: Your toolkit and the course project. It sets up the browsers and DevTools settings you'll use for the rest of the course, and takes a first baseline.