Designing for the device you don't own.
Your laptop has a fast processor, plenty of memory and a cable to the office router. Most of the people paying for your product have none of those. A speed target only means something once it names the phone and the connection it is judged on, and a well-built page notices when it is running on something weaker still and quietly asks it for less.
Builds on Core Web Vitals as requirements.
The idea.
The page you test is not the page your users get. The difference is the hardware and the radio between them and you.
Cobbleway's front-end team works on recent laptops wired to the office network. When one of them opens the product page for a gravel wheelset, the main photo is up before they have let go of the trackpad. Meanwhile a rider standing at a trailhead with a two-year-old Android phone and two bars of signal opens the same URL and stares at a grey box for three and a half seconds. Both people loaded identical HTML, identical scripts and an identical photo. Only the machine and the connection changed.
Two different things went slower. The network part grows with the distance in time between the phone and the server: every round trip a cellular link takes is paid again for the connection, the HTML, then the files the HTML asks for. The processor part grows with how much script the page runs: a phone's cores are slower, it has less cache, and it turns its clock down when it gets warm, so the same JavaScript takes several times longer to parse and run. A laptop on fibre hides both bills at once.
So a performance requirement needs two more words than most teams write: on what, over what. The device and the network are picked from your own traffic, stand in for the slower part of it, get reproduced in the lab with throttling, and appear in the requirement itself. And because no single target device covers everyone, the page can also read a few hints at runtime (a data-saving preference, a rough memory size, a connection estimate) and ask a weak phone for less.
The same product page on three setups
- Waiting on the network
- Waiting on the CPU
Data
| Setup | Waiting on the network (s) | Waiting on the CPU (s) |
|---|---|---|
| Laptop, fibre | 0.6 | 0.2 |
| Mid-range, 4G | 2.1 | 1.3 |
| Low-end, weak 4G | 3.4 | 2.5 |
- LCP good threshold: Time to the main photo (LCP) (s) = 2.5
Where the laptop lies
| No. | Area | Requirement | Target / measure |
|---|---|---|---|
| 01 | Metric and threshold | What is measured and the line it must stay under. | LCP ≤ 2.5 s Thresholds come from Core Web Vitals as requirements. |
| 02 | Percentile and segment | Which share of which visits must meet it. | p75phones Phones and desktops are judged apart; the phone segment is the hard one. |
| 03 | Reference device | The phone the lab reproduces and the team owns physically. | 4 GB mid-range Android Chosen from traffic so the slowest quarter of visits is covered (in practice, below). |
| 04 | Reference network | The connection the lab reproduces. | Slow 4G150 ms RTT1.6 Mbps Close to the field p75 round trip; Lighthouse's default mobile profile. |
How it works.
Two bills that grow on weaker hardware, where to find out what your users really hold, how the lab imitates it, and what a page can learn about the device at runtime.
The processor bill
Everything a page does after the bytes arrive runs on the device: parsing HTML and CSS, compiling and running JavaScript, working out layout, decoding images and painting. A recent laptop has a few large, fast cores, generous cache, and a fan. A mid-range phone has a mix of big and small cores, much less cache, and a thermal limit, so after a few seconds of heavy work it slows itself down. None of this shows up in a bundle size, which is why two pages with the same byte count can feel completely different on a phone.
Script is where the gap bites hardest, because a byte of JavaScript costs CPU time to compile and run on top of its download (the full pipeline is in What a byte of JavaScript costs). When the V8 team ran one large site's script on phones of three price levels, the median phone needed three to four times as long as the high-end one and the cheapest phone more than six times. Treat those ratios as an order of magnitude, not a constant: your own trace on your own reference phone is the number to plan with.
Memory matters in a quieter way. A phone with 2 or 3 GB of RAM keeps fewer tabs and decoded images alive, so a heavy page is more likely to be evicted and reloaded from scratch when the rider switches to the maps app and back. That is one reason a memory reading is useful as a hint even though it says nothing about clock speed.
The network bill is two numbers
A connection has a round-trip time (how long a tiny packet takes to get to the server and back) and a bandwidth (how many bytes per second flow once data is moving). Speed tests advertise bandwidth, but the start of a page load is dominated by round trips: looking up the name, opening TCP, agreeing TLS keys, asking for the HTML, then asking for whatever the HTML points to. Each step waits a full round trip before the next can begin. On office fibre that is about 10 ms per step; on a phone in a valley it is 150 ms or more, and a weak signal adds retransmissions on top.
The first five steps of a load, on 4G and on fibre
Scenario 1 of 2: As described.
Timeline as a list
The first five steps of a load, on 4G and on fibre: 3 lanes, from 0 ms to 1,500 ms.
- 0–150 ms · Connection (DNS, TCP, TLS) · DNS
- 0–450 ms · all lanes · window: 3 round trips before any request
- 150–300 ms · Connection (DNS, TCP, TLS) · TCP
- 300–450 ms · Connection (DNS, TCP, TLS) · TLS
- 450–700 ms · HTML document · GET + server
- 700–800 ms · HTML document · 20 KB
- 700 ms · HTML document · first byte
- 800–950 ms · CSS and the wheelset photo · request
- 950–1,380 ms · CSS and the wheelset photo · 86 KB (CSS 16 + photo 70)
- 1,380 ms · CSS and the wheelset photo · photo bytes in (ok)
Where the real distribution comes from
You cannot pick a reference device from a guess about "typical users". Two sources tell you what your visitors actually hold. Your own field data (real-user monitoring, covered in Measuring real users) can record the user agent, a memory reading, a core count and a connection estimate next to every timing, so you can sort visits from slowest to fastest setup. The Chrome UX Report (CrUX) gives the same view for any public origin, but only for eligible Chrome users and only along a few dimensions.
The rule for choosing is the percentile rule turned around. A target judged at p75 is decided by the slowest quarter of visits, so the reference device and network should sit at about the 25th percentile of capability: weaker than most of your traffic, not the median phone and certainly not the median laptop. Percentile maths in general is in Latency and percentiles.
What each source can tell you about devices and networks
| Source | Device information | Network information | Blind spots |
|---|---|---|---|
| Your own RUM | User agent, navigator.deviceMemory and hardwareConcurrency where the browser exposes them | navigator.connection (effectiveType, rtt, downlink) in Chromium; the timings themselves everywhere | Memory and connection are missing on Safari and Firefox; you only see users who stayed long enough to send a beacon |
| CrUX | Form factor only: phone, tablet or desktop, inferred from the user agent | Round trip time, binned low (0-74 ms), medium (75-274 ms) and high (275 ms and above); the old effective connection type dimension was retired in February 2025 | Chrome users who opted in, not iOS or WebView; no device model or memory |
| Analytics and app-store stats | Device models and OS versions by share | Usually none | Counts users, not page loads; says nothing about speed |
Lab profiles are approximations
Nobody can run every pull request on a drawer of real phones, so the lab imitates the reference setup on a fast machine. Network throttling adds latency and caps bandwidth; CPU throttling slows the page's processes by a multiplier. Both are approximations. A 4x slowdown on a fast laptop and a 4x slowdown on an older desktop end up at different places, and a request-level latency rule does not reproduce how a real radio wakes up, loses packets or switches cells.
Three habits fix most of this. Calibrate the CPU multiplier against your own machine instead of guessing: Lighthouse lets you derive it from its benchmark index, and since Chrome 134 DevTools can run a short benchmark and create low-tier and mid-tier mobile presets for that computer. Keep the network profile close to your field round trip (Lighthouse's default mobile profile, now called Slow 4G, is 150 ms and 1.6 Mbps). And check the lab against the field now and then on one real reference phone, because the field p75 is what the requirement is judged on.
Ways to imitate the reference setup
| Option | What it does | Good for | Watch out |
|---|---|---|---|
| Lighthouse simulated (default) | Loads the page unthrottled, then models the load on 150 ms RTT, 1.6 Mbps down, 750 Kbps up and a 4x CPU | Fast, repeatable CI runs | A model: it can be wrong for pages with unusual request chains |
| DevTools network presets | Adds latency and caps throughput per request in the browser | Seeing the waterfall by hand while debugging | Request-level, so it misses packet loss and connection effects |
| DevTools calibrated CPU presets | A one-off benchmark of your machine produces low-tier and mid-tier mobile multipliers | Profiling script and rendering cost as a phone would feel it | Specific to the machine that ran the calibration |
| Packet-level throttling | Shapes traffic at the OS or network level | The most faithful network imitation | More set-up, more run-to-run variance |
| A real reference phone | The actual device over a real or shaped connection | Calibrating everything above; spot checks before a launch | Slow to run, hard to automate at scale |
Signals a page can read at runtime
A reference device fixes what you design and test for. It does not stop a rider with a 2 GB phone from showing up. Adaptive loading closes that gap: the page, or the server, reads a few coarse hints about this particular visit and leaves out or postpones the heaviest optional parts. Every hint is deliberately vague, and most exist only in Chromium-based browsers, so each one is something to act on when present, never something to require.
The hints, what they mean and where they exist (October 2026)
| Signal | What it tells you | Where it works | Privacy and accuracy |
|---|---|---|---|
| Save-Data: on (request header) | The user switched on a reduced-data setting in the browser or the system | Chromium-based browsers, on the first request without the server asking; not Safari or Firefox | Low entropy. An explicit choice, so honour it; add Vary: Save-Data to responses that change |
| navigator.connection.saveData | The same preference, readable from script | Chromium only | Same as the header |
| navigator.connection.effectiveType | slow-2g, 2g, 3g or 4g, estimated from recently observed round trips and throughput | Chromium only, in windows and workers, with a change event | 4g covers every link under a 270 ms round trip with more than 700 kbps down, so it cannot tell good 4G from fibre |
| navigator.connection.rtt / downlink | The estimates behind effectiveType, in ms and Mbps | Chromium only | Rounded (25 ms, 25 kbps steps) to blunt fingerprinting |
| navigator.deviceMemory | Approximate RAM in GiB, as a power of two | Chromium only, HTTPS only | Clamped to bounds each browser sets and has changed over time; never an exact size |
| navigator.hardwareConcurrency | Logical processors available for threads | Every major browser | May be lower than the real count; says nothing about how fast each core is |
| ECT, RTT, Downlink, Sec-CH-Device-Memory (client hints) | The values above, sent as request headers | Chromium only, after the server asks with Accept-CH | Not on the very first request unless Critical-CH triggers a retry; varying on them hurts caching |
| @media (prefers-reduced-data: reduce) | A data-saving preference for CSS | Specified but shipped in no browser | Do not rely on it yet |
Reading the hints with safe defaults
// Cobbleway decides once per page view how much optional weight to load.
// Every signal can be missing; missing means "no evidence", not "weak".
export function readLoadHints() {
const conn = navigator.connection; // undefined in Safari and Firefox
const memory = navigator.deviceMemory; // undefined outside Chromium or over HTTP
const cores = navigator.hardwareConcurrency || 0;
const saveData = conn?.saveData === true;
const slowNetwork = conn !== undefined &&
['slow-2g', '2g', '3g'].includes(conn.effectiveType);
const weakDevice = (memory !== undefined && memory <= 2) ||
(cores > 0 && cores <= 2);
return {
saveData, // a choice: always honour it
lite: saveData || slowNetwork || weakDevice,
reported: conn !== undefined || memory !== undefined,
};
}
let hints = readLoadHints();
navigator.connection?.addEventListener('change', () => {
hints = readLoadHints(); // affects what has not started yet
});
# First response: ask for the hints on later requests
HTTP/1.1 200 OK
Accept-CH: ECT, Sec-CH-Device-Memory
Vary: Save-Data
Content-Type: text/html
# A later image request from a phone with data saving on
GET /img/wheelset-hero.avif HTTP/1.1
Host: shop.cobbleway.example
Save-Data: on
ECT: 3g
Sec-CH-Device-Memory: 2
The hints feed a decision about optional weight, never about what a visitor is allowed to do. The rule that keeps adaptive loading honest is to degrade a feature, not remove it: a lighter version stays one tap away, a preference the user stated wins over any guess, and a change of connection halfway through only affects work that has not started. The machine below is the life of one heavy feature on a page under that rule.
One optional heavy feature during a page view
Notice that every path from Lite can still reach Ready: the rider can always ask for the full version. A failure falls back to the light version rather than to an empty box.
| From → To | Event | Guard | Action |
|---|---|---|---|
| Reading hints → Full | hints read | no saving or weak signal | |
| Reading hints → Lite (light stand-in shown) | hints read | saveData or slow net or ≤ 2 GB | |
| Full → Lite (light stand-in shown) | connection change | now 2g and not started | |
| Full → Loading the feature | near viewport | fetch script and assets | |
| Lite (light stand-in shown) → Loading the feature | rider taps the button | fetch script and assets | |
| Loading the feature → Ready | loaded | swap stand-in for feature | |
| Loading the feature → Lite (light stand-in shown) | error or timeout | keep stand-in, offer retry |
- Reading hintsstart
- Runs once, before any optional request
- Full
- The feature will load when it scrolls near the viewport
- Lite (light stand-in shown)
- A static stand-in plus a button that names the cost
- Readyend
In practice.
Cobbleway picks its reference phone from its own traffic, writes it into the requirement, and builds a lighter wheelset page for riders who save data or carry a weak phone.
Picking Cobbleway's reference phone
The team starts from 28 days of its own field data. Seven visits in ten come from phones, so the phone segment is the one the requirement must hold for. Safari does not report memory, so iPhone visits are placed using the model mix from the shop's analytics; every iPhone model Cobbleway sees is faster than its 4 GB Android tier, which puts them all on the fast side of the line. Then the Android visits are sorted by reported memory, slowest first, until a quarter of phone visits is covered.
Where the slowest quarter of phone visits ends
- Android share of phone visits
- 62%The other 38% are iPhones; illustrative
- Android visits reporting 2 GB or less
- 9%
- Android visits reporting 4 GB
- 34%
- CrUX phone round trip at p75
- 160 msIn the medium bin (75-274 ms); illustrative
- Lighthouse Slow 4G round trip
- 150 msCited
- Phone visits on 2 GB or less62% × 9%5.6%from Android share of phone visits and Android visits reporting 2 GB or less
- Phone visits on 4 GB62% × 34%21.1%from Android share of phone visits and Android visits reporting 4 GB
- Slowest tiers together5.6% + 21.1%26.7%from Phone visits on 2 GB or less and Phone visits on 4 GB · The 25th percentile of capability falls inside the 4 GB tier
- Lab round trip vs field p75150 ÷ 160within 7%from CrUX phone round trip at p75 and Lighthouse Slow 4G round trip · Slow 4G is a fair lab stand-in
- The reference phone is a 4 GB, 8-core Android from 2022, the most common model in that tier. The team buys two and keeps them charged in the office.
- The 2 GB phones (5.6% of phone visits) are the floor: the shop must work on them, and adaptive loading is aimed at them, but the p75 target is not set by them.
- Lighthouse's default mobile profile already matches the field round trip, so the CI profile stays as it is; only the CPU multiplier is recalibrated on the CI machine.
| No. | Area | Requirement | Target / measure |
|---|---|---|---|
| 01 | Reference device | Lab runs, profiling and pre-launch checks use this phone or a calibrated imitation of it. | 4 GB8-core Android (2022) Re-checked every quarter against the field mix; the slowest-quarter line moves as riders upgrade. |
| 02 | Reference network | CI and manual tests run on this profile. | Slow 4G150 ms RTT1.6 Mbps down Field phone RTT p75 is 160 ms. |
| 03 | Lab CPU | The CI machine's CPU is slowed to the reference phone's speed. | calibrated mid-tier preset Recalibrated whenever the CI hardware changes. |
| 04 | Floor device | Every task (browse, filter, add to basket, pay) works; speed targets do not apply. | 2 GB Android3g effective type Adaptive loading exists for this group. |
| 05 | Judged on | The field p75 for phone visits, from RUM and CrUX. | field p75phones Lab results warn early; the field decides. Thresholds live in Core Web Vitals as requirements. |
A lighter wheelset page
Cobbleway's wheelset pages carry the heaviest optional feature in the shop: a 3D viewer that lets a rider turn the wheel and inspect the hub. Its script and model weigh 4.2 MB, and on a 2 GB phone it competes for memory with everything else. The six gallery photos add about 1.1 MB more at full quality. None of that is needed to choose a hub fitting and add the wheels to the basket.
So the page has two weights. The full page is what the reference phone gets. The light page swaps the viewer for its first photo and a button that says what pressing it will cost, and serves three gallery photos at lower quality. Everything a rider can do on one, they can do on the other.
The same product page for two riders
Same HTML route, same prices, same buttons. The 8 GB phone queues the viewer for when the gallery is in view and loads six photos at full quality (about 5.3 MB in all); the data-saving phone loads three lighter photos (about 210 KB) and no viewer.
Two riders, one URL. 8 GB phone on 4G, no data saving: Store “Cobbleway”, showing the product view. Cart button “Basket” with badge 0 (its accessible name says “Cart, 0 items”, not the number alone). Product (showing) “Ridgeline G30 gravel wheelset” by “700c · tubeless ready · 1,540 g the pair”: rating 4.6 (“212 reviews”); price £389.00. Gallery: image 1 of 6 (“3D view: drag to turn the wheel”); thumbnails beside it. Hub fitting (tiles, a radio group): Shimano HG, SRAM XDR, Campagnolo N3W. Buttons: “Add to basket”. Delivery: “Free delivery over £50 · arrives Thu 8 Oct”. Folded sections: Specs, Fitting guide, Reviews. Note on gallery: 3D viewer + 6 photos ≈ 5.3 MB 2 GB phone, data saving on: Store “Cobbleway”, showing the product view. Cart button “Basket” with badge 0 (its accessible name says “Cart, 0 items”, not the number alone). Product (showing) “Ridgeline G30 gravel wheelset” by “700c · tubeless ready · 1,540 g the pair”: rating 4.6 (“212 reviews”); price £389.00. Gallery: image 1 of 3 (“Side view of the wheel (lighter photo)”); thumbnails beside it. Hub fitting (tiles, a radio group): Shimano HG, SRAM XDR, Campagnolo N3W. Buttons: “Add to basket”, “3D view · 4.2 MB”. Delivery: “Free delivery over £50 · arrives Thu 8 Oct”. Folded sections: Specs, Fitting guide, Reviews. Note on gallery: saveData: true · 3 photos ≈ 210 KB
- Heavy on capable phones, light under data saving
- Same buttons; the light page adds an opt-in with its size
Who decides, the server or the page
Some choices have to be made before the page's script runs. The HTML decides which image URLs the browser starts fetching, and by the time JavaScript could change them the full-quality photos are already on the way. In the Chromium-based browsers that send it, Save-Data is the one hint that arrives on the very first request without being asked for, so Cobbleway's server uses it for the HTML and the images. The other hints reach the server only after it has asked for them with Accept-CH, and only from Chromium browsers, so decisions about optional features stay in the page, where load-hints.js reads them.
A data-saving visit to the wheelset page
- Rider's phone → Cobbleway server: GET /p/ridgeline-g30 · Save-Data: on
- Note over Cobbleway server: Only Save-Data is known on this first request; memory and connection hints come later, if at all
- Cobbleway server → Rider's phone (reply): light HTML (3 photo URLs, no viewer tag) · Accept-CH: ECT, Sec-CH-Device-Memory · Vary: Save-Data
- Rider's phone → Image service (same origin): GET /img/wheelset-1.avif · Save-Data: on · ECT: 3g · Sec-CH-Device-Memory: 2
- Image service (same origin) → Rider's phone (reply): 70 KB lower-quality photo · Vary: Save-Data
- Note over Rider's phone: load-hints.js: saveData true, so the 3D viewer is not requested
- Rider's phone → Cobbleway server: Rider taps the 3D view button: GET /3d/ridgeline-g30.glb
- Cobbleway server → Rider's phone (reply): 4.2 MB model and viewer script
- Pro:The first response is already light for riders who asked for it
- Pro:Optional features use every hint the browser offers, including the change event
- Pro:Only one cache dimension (Save-Data), which has two values
- Con:Two places hold adaptive logic and must agree
- Con:The server only sees the explicit preference, not a weak device
Hints other than Save-Data are missing on the first request unless Critical-CH forces a retry; Varying on fast-changing network hints splits the cache into many copies; Chromium only, so every other browser gets the default anyway
Images named in the HTML are already downloading before script runs; The decision waits for the script, which is slowest on exactly the phones it is meant to help
Saying it in an interview
System flows that use a reference device and adaptive loading
| Flow | Where it matters | What our design for it adapts |
|---|---|---|
| amazon/product-detail-page | A large image gallery, zoom and spin views on a phone over mobile data | Gallery image quality and count; zoom and 360 views load on request |
| amazon/product-listing | Dozens of thumbnails in a grid on a mid-range phone | Thumbnail quality and how far ahead the next page is prefetched |
| airbnb/listing-page | A photo tour and a map above the fold | Photo sizes; the interactive map waits for a tap on weak devices |
| netflix/resume-and-previews | Autoplaying previews on a TV-like home screen in a browser | Previews stay as still artwork under data saving or a slow connection |
| instagram/infinite-photo-feed | Endless images and video in a feed | Prefetch depth and video autoplay under data saving |
| facebook/news-feed | A long feed mixing text, images and video | Autoplay off and lighter media under data saving; same posts either way |
Trade-offs.
A reference device is cheap to adopt and easy to forget; adaptive loading is powerful and easy to overdo.
What adaptive loading buys and what it costs
How it goes wrong
| Pitfall | What happens | Instead |
|---|---|---|
| The reference device drifts | The phone chosen two years ago is now faster than most of the slowest quarter, so the lab passes while the field p75 slips | Re-derive it from field data on a schedule (Cobbleway: every quarter) and when the field and lab disagree |
| Missing hint read as weak | Every iPhone and Firefox user gets the light page, including the fastest phones in your traffic | Treat a missing value as unknown and take the default path |
| The light page becomes a lesser product | Low-end users lose search filters, reviews or checkout options and convert worse, which looks like proof they are less valuable | Adapt only optional weight; keep every task, and offer an opt-in to the heavy version |
| Logging raw hints for analytics | Memory, cores, connection and user agent together narrow a visitor down | Record coarse buckets next to timings; never combine them into an identifier |
| Varying the cache on network hints | A response stored per ECT and RTT value is close to uncacheable at the CDN | Vary on Save-Data only (two values) and adapt the rest in the page |
| Trusting effectiveType for the fast end | 4g covers everything below a 270 ms round trip and above 700 kbps, so a slow 4G link and fibre look the same | Use it to spot weak connections; measure fast ones in the field instead |
| A light path nobody tests | A bug in the data-saving branch ships unnoticed for weeks | Run CI on both paths, and give the team a toggle to force the light page |
- Pro:Works the same in every browser
- Pro:The requirement is met without any hint
- Pro:Hints only make Chromium's weakest visits lighter still
- Con:The default must be disciplined; adaptive loading cannot rescue a heavy default
Downgrades every Safari and Firefox visitor, including the fastest phones; Makes the light path the main path by accident
Reduced user agents no longer name the model reliably; A model list goes stale and needs constant upkeep