How fast is fast enough.
"It should feel fast" is not a requirement, because nobody can fail it. Human attention gives you better numbers: a reaction inside about a tenth of a second feels caused by the tap, a wait under about a second keeps the train of thought, and past about ten seconds the mind wanders off. Give every interaction one of those limits, and the limit tells you what the screen must show while the work is still going.
The idea.
People do not experience milliseconds. They experience whether a tap answered them, whether a wait broke their thought, and whether they gave up and looked away.
Picture a rider on a commuter train with Cobbleway open on a mid-range phone. She taps Add to basket on a chain. If the basket badge changes before her thumb has left the glass, the shop feels like an object she is handling. If nothing moves for half a second she wonders whether the tap registered, and often taps again. If the category page takes three seconds to show resin pads after she picks the filter, she has already glanced out of the window, and part of her attention has to find its way back.
Those three moments sit on three different limits of human perception, and they have been remarkably stable for half a century. Robert Miller analysed how long people tolerate a terminal's reply in 1968; Card, Robertson and Mackinlay built on perception research for their 1991 information workspace; Jakob Nielsen folded both into three round numbers in 1993: about 0.1 s, about 1 s and about 10 s. Google's RAIL model later restated them for browsers. The numbers are approximate, and they are about people, not networks: a fibre connection does not make a person notice delays any later.
The design consequence is the useful part. Each limit tells you what the interface owes the person while the work is still running. Inside 100 ms the result itself can be the feedback. Up to a second the screen must at least admit the request was heard. Beyond that it must show that progress is happening, and beyond ten seconds it must let the person do something else. So a performance requirement does not say "the product page is fast". It lists the interactions and gives each a band.
The limits, and what each one owes the user
| Limit | What the person experiences | What the interface owes them | A Cobbleway interaction in this band |
|---|---|---|---|
| one frame: 16.7 ms at 60 Hz, 8.3 ms at 120 Hz | Motion looks continuous; a missed frame reads as a stutter | A new frame every refresh while something moves | Dragging the price-range slider on a category page |
| ~100 ms | The response feels caused by the action | The result itself or a visible reaction of the control | Pressing a button, picking a size, the basket badge after Add to basket |
| ~1 s | The delay is noticed but the thought is not lost | Acknowledge at once; keep the old content visible or hold its space | Applying a filter; opening a product from the list |
| ~10 s | Attention stays on the task but patience drains | A looped busy indicator and words for what is happening | Paying for an order |
| beyond 10 s | Attention leaves; people switch tabs or abandon | Progress with an estimate, a way to cancel, a way to come back later | Uploading a photo of a worn part to the part finder |
The limits on one scale
Data
| Limit | ms |
|---|---|
| Frame at 120 Hz | 8.3 |
| Frame at 60 Hz | 16.7 |
| Feels instant | 100 |
| Flow unbroken | 1,000 |
| Attention held | 10,000 |
How it works.
Sort each interaction by what kind of thing it is, look up the limit it lives under, then decide what the screen shows while the real work runs. The last step is where perceived time and measured time part ways.
Three kinds of interaction
Interactions are not equal, and the person's expectation depends on what they think they did. Touching a control is direct manipulation: the rider expects the thing under her finger to react as a physical switch would, so it belongs to the 100 ms band, and anything that moves under her finger belongs to the frame. Asking for something new, a page, a filtered list, a search, is a request: she knows the shop has to go and get it, so the 1 s band applies, as long as the screen admits the request at once. Work she knows is heavy, paying, uploading, generating a report, is a job: the 10 s band applies, and past it the interface has to stop holding her hostage.
Most real interactions are a mix. Add to basket is a tap (the button must react in 100 ms) that starts a request (the basket must be right within a second or so). Writing the two halves down separately is what makes the requirement testable: "the button shows it was pressed in the next frame" and "the basket badge shows the new count within 1 s at p75" are two different promises, measured from the same tap.
Kinds of interaction and their limits
| Kind | Examples | Limit | Measured from | Measured to |
|---|---|---|---|---|
| Continuous motion | Scrolling, dragging a slider, swiping a gallery, an animated drawer | every frame | each input or animation tick | the next frame painted |
| Direct manipulation | Tap a button, tick a checkbox, pick a size, type in a field | ~100 ms | the input event | the control visibly reacting |
| Request | Open a product, apply a filter, run a search, go to the next page | ~1 s | the input event | the useful content painted |
| Job | Place an order, upload a photo, export order history | ~10 s and beyond | the input event | the outcome shown or handed to a notification |
The frame inside the 100 ms
The screen only changes when the browser paints a frame. At 60 Hz a frame comes every 1000 / 60 = 16.7 ms; on the 120 Hz screens now common on phones it is 1000 / 120 = 8.3 ms. The browser needs part of each frame for style, layout and paint, so MDN and RAIL both suggest your own work per frame stays near 10 ms on a 60 Hz screen. How the event loop picks when to paint is taught in the browser-rendering topic on the frame and the event loop; here it matters for two reasons.
First, the 100 ms limit is about six frames at 60 Hz. RAIL turns that into a rule of thumb: finish handling input within 50 ms, so that, together with whatever task was already running when the tap arrived, the change still reaches the screen inside 100 ms. Second, while something moves, the budget is the frame itself, not 100 ms: a drag that lags by three frames feels like dragging through mud even though 50 ms sounds fast.
How many frames fit inside 100 ms
- 1, width 17, Frame with nothing new to show
- 2, width 17, Frame with nothing new to show
- 3, width 17, Frame with nothing new to show
- 4, width 17, Frame with nothing new to show
- 5, width 16, Frame with nothing new to show
- 6, width 16, Frame with nothing new to show
- tap, pointer at 0
- width 8, Frame with nothing new to show
- width 8, Frame with nothing new to show
- width 9, Frame with nothing new to show
- width 8, Frame with nothing new to show
- width 8, Frame with nothing new to show
- width 9, Frame with nothing new to show
- width 8, Frame with nothing new to show
- width 8, Frame with nothing new to show
- width 9, Frame with nothing new to show
- width 8, Frame with nothing new to show
- width 8, Frame with nothing new to show
- width 9, Frame with nothing new to show
- tap, pointer at 0
- Frame with nothing new to show
- Main thread busy
- Frame that shows the response
As it starts. 3 steps follow.
The 100 ms limit is not INP's 200 ms
Interaction to Next Paint, the Core Web Vitals responsiveness metric, calls 200 ms or less at the 75th percentile good. That is not a contradiction. The 100 ms limit is what one person perceives on one tap; INP is a field threshold over many visits on real devices, set where most sites can reach it. A requirement can name both: design each tap to react within 100 ms on the reference phone, and accept the page as passing when field INP at p75 is 200 ms or less. The vitals topic owns the metric; this topic owns the design target.
What each band owes the user
Expected wait and the feedback that fits it
| Expected wait | Feedback | Why | Avoid |
|---|---|---|---|
| under ~100 ms | The result itself, or the control's pressed state | Anything more is noise; the change is the acknowledgement | A spinner that appears for two frames |
| ~100 ms to ~1 s | Inline acknowledgement: a busy label on the button, the old list dimmed, a chip that appears at once | The rider needs to know she was heard; she does not need to be told to wait | Clearing the screen, or an animated indicator that flashes on and off (NN/g advises none under about 1 s) |
| ~1 s to ~10 s | A looped indicator with words ("Confirming payment…"), or a skeleton that holds the shape of what is coming | The wait is noticeable; the screen must prove work is happening | A frozen screen; an indicator with no words |
| over ~10 s | Determinate progress (percent or steps), an estimate, Cancel, and a way to leave and be told later | In a study NN/g cites, people shown a moving progress bar were more satisfied and waited about three times longer than people shown nothing | A spinner that turns for a minute with no end in sight |
Do not flash a spinner
The bands describe the expected wait, but any one request lands anywhere in its distribution: Cobbleway's filter request comes back in 250 ms on a good train stop and 2 s in a tunnel. Show a spinner the moment the request starts and the fast case gets a spinner that blinks on and off, which reads as a glitch and makes a quick answer feel slower. The usual fix is two timers. Show the indicator only if the request is still pending after a short delay, and once it is shown keep it up for a minimum time, so it never flickers. Cobbleway picks 300 ms and 500 ms; neither is a standard, both are design choices you would state and tune.
The delay applies to animated indicators only. The tap itself still owes a reaction inside 100 ms: the button's pressed state, a busy label, the filter chip appearing. A delayed spinner with no immediate acknowledgement is the worst of both, because the rider sees nothing for 300 ms and taps again.
A loading indicator that never flashes
One request's indicator. Fast answers settle before the 300 ms delay and never show it; slow ones show it for at least 500 ms. Every state keeps the button's busy label, which appeared on the tap.
| From → To | Event | Guard | Action |
|---|---|---|---|
| Idle → Pending, indicator hidden | tap | busy label; start 300 ms timer | |
| Pending, indicator hidden → Result shown | answer | before 300 ms | show result |
| Pending, indicator hidden → Error shown | error | before 300 ms | |
| Pending, indicator hidden → Indicator shown | 300 ms timer | show indicator | |
| Indicator shown → Result shown | answer | shown 500 ms or more | |
| Indicator shown → Answer ready, holding indicator | answer | shown under 500 ms | |
| Answer ready, holding indicator → Result shown | 500 ms minimum reached | show result | |
| Indicator shown → Error shown | error |
- Idlestart
- Pending, indicator hidden
- The control already shows it was pressed
- Answer ready, holding indicator
- Waits out the minimum so it does not flicker
- Result shownend
- Error shownerror
The two timers in code
// Show `onShow` only if `work` is still pending after `delayMs`,
// and once shown keep it for at least `minVisibleMs`.
export async function withIndicator<T>(
work: Promise<T>,
onShow: () => void,
onHide: () => void,
{ delayMs = 300, minVisibleMs = 500 } = {},
): Promise<T> {
let shownAt: number | null = null;
const timer = setTimeout(() => {
shownAt = performance.now();
onShow();
}, delayMs);
try {
return await work;
} finally {
clearTimeout(timer);
if (shownAt !== null) {
const left = minVisibleMs - (performance.now() - shownAt);
if (left > 0) await new Promise((r) => setTimeout(r, left));
onHide();
}
}
}
// Usage on the brake-pads page: the chip and the dimming happen
// synchronously in the click handler; only the spinner is delayed.
// const results = await withIndicator(fetchPads(filters), showSpinner, hideSpinner);
Perceived time versus measured time
Measured time runs from the tap to the moment the work is truly done. Perceived time runs from the tap to the moment the rider believes it is done, or at least believes it is under way. The two can differ by most of a second, and the gap is a design choice, not an accident. Three levers widen it in your favour.
- Optimistic update: show the expected result at once and reconcile when the server answers. It fits writes that almost always succeed and are cheap to undo, like a basket line or a like. The snapshot, reconcile and rollback machinery belongs to the data-retrieval topic on mutations; what matters here is that it moves a request-sized wait into the 100 ms band.
- Placeholders that hold the shape: a skeleton, or the previous content kept on screen but dimmed. The rider sees where the answer will appear and keeps her place. A skeleton must reserve exactly the space of the real content, or the swap becomes a layout shift.
- Progressive rendering: show the first useful part (the product title, price and photo) before the rest (reviews, related parts). The page enters the 1 s band for what matters even if the whole page takes longer.
None of these make the network faster, and none of them excuse a slow server: an optimistic basket that is wrong one time in twenty feels broken, and a skeleton shown for five seconds is still a five-second wait. They decide which band the rider experiences while the real work catches up.
One Add to basket, five ways it can look
Scenario 1 of 5: As described.
- 6 A second add is now in flight; the basket may end up with two chains.
Timeline as a list
One Add to basket, five ways it can look: 4 lanes, from 0 ms to 1,200 ms.
- 0–620 ms · What the screen shows · no visible change
- 0 ms · Rider · taps Add to basket
- 20 ms · Basket code in the browser → Basket API, arriving 150 ms: POST line
- 100 ms · all lanes · 100 ms limit (deadline)
- 150–490 ms · Basket API · add line
- 450 ms · Rider · nothing happened? taps again (error) — A second add is now in flight; the basket may end up with two chains.
- 490 ms · Basket API → Basket code in the browser, arriving 620 ms: 200 basket v8
- 620 ms · What the screen shows · badge 1 to 2 (ok)
In practice.
Cobbleway's performance requirement starts as an inventory of interactions, each with a band, a target on the reference phone and the feedback it owes. That inventory is also the shape of a strong interview answer.
List the interactions a rider actually performs on the three pages that matter, home, category and product, plus checkout. For each, write the band, the target and how it is measured (at p75 on the reference device, a mid-range Android phone on 4G, chosen in the device topic), and the feedback the screen shows inside the 100 ms limit whatever the network does. The page-load numbers themselves are Core Web Vitals and live in that topic; this table covers what happens after the page is up.
| No. | Area | Requirement | Target / measure |
|---|---|---|---|
| 01 | Any tap on a control | The control shows it was pressed in the next frame, and its result (or an acknowledgement) is painted within 100 ms. | pressed state next framevisible response ≤ 100 msfield INP ≤ 200 ms at p75 Reference phone, 60 Hz. INP is the field check; the 100 ms is the design target per control. |
| 02 | Add to basket (product page) | The basket badge shows the new count at once, optimistically; the server confirms within 1 s at p75; a failed add rolls back with a message. | badge next frameconfirm ≤ 1 s at p75rollback rate ≤ 0.5% Assumed round trip 620 ms at p75 on 4G. A rollback rate above the limit means the bet is wrong; switch to a busy label. |
| 03 | Filter change (category page) | The chip and the result count placeholder appear at once; the old results stay, dimmed; new results replace them within 1 s at p75. | chip next frameresults ≤ 1 s at p75no layout shift on swap Assumed p75 800 ms. Indicator only after 300 ms, then for at least 500 ms. |
| 04 | Open a product from the list | Title, price and main photo are painted within 1 s at p75 for a soft navigation; reviews and related parts may follow. | first useful paint ≤ 1 s at p75skeleton holds layout The first visit to the product page is a page load, governed by LCP in the vitals topic. |
| 05 | Place order (checkout) | The button turns busy at once and cannot be pressed twice; a worded indicator appears after 1 s; the outcome is shown within 10 s at p95. | busy state next frameindicator after 1 soutcome ≤ 10 s at p95 Assumed p75 3.2 s (payment provider included). Never optimistic. |
| 06 | Photo part finder | Upload shows determinate progress with a Cancel; the rider may leave the page and is told when matches are ready. | progress updates ≥ 1 per secondcancel always availableresult kept for 24 h Assumed 14 s on 4G for a 2.5 MB photo plus matching; the only interaction expected to pass 10 s. |
Add to basket, as the rider sees it
One item already in the basket. The rider has picked 118 links.
Before the tap. Store “Cobbleway”, showing the product view. Cart button “Basket” with badge 1 (its accessible name says “Cart, 1 item”, not the number alone). Product (showing) “Trailhead 11-speed chain” by “Cobbleway Workshop range”: rating 4.6 (“318 reviews”); price £27.50. Gallery: image 1 of 4 (“Trailhead chain, coiled, side view”); thumbnails beside it. Length (tiles, a radio group): 114 links, 118 links (selected), 126 links. Stock: “In stock”. Quantity 1 (stepper “Quantity”). Buttons: “Add to basket”, “Buy now”. Delivery: “Dispatched today if ordered by 3 pm”. Folded sections: Fit and compatibility, Reviews, Delivery and returns.
- Basket badge: optimistic, then the server's count
- Button: pressed and busy within one frame
A filter change inside the 1 s band
All 64 pad sets, sorted by popularity.
Loaded. Grid, 3 columns: 6 items. Search: “Disc brake pads”. Sort: Sort: Popular. Count: 64 results. Facet Compound (check): Resin (24), Sintered (28), Semi-metallic (12) Facet Price (range): range £8 to £45 1. Trailhawk resin pads · £24.90 [image 1:1] badges: Best seller. 2. Quietstop resin pads · £14.00 [image 1:1] 3. Ridgeline sintered pads · £29.50 [image 1:1] 4. Longhaul sintered pads · £18.50 [image 1:1] 5. Mudlark resin pads · £18.00 [image 1:1] 6. Fellrunner semi-metallic pads · £32.00 [image 1:1] Footer: page links.
- Acknowledges the tap within 100 ms
- Says the list is updating
Cobbleway's interactions against their limits
- Measured p75 (assumed)
- Band limit
Data
| Interaction | Measured p75 (assumed) (ms) | Band limit (ms) |
|---|---|---|
| Tap feedback | 40 | 100 |
| Add to basket | 620 | 1,000 |
| Filter change | 800 | 1,000 |
| Open product | 900 | 1,000 |
| Place order | 3,200 | 10,000 |
| Part finder | 14,000 | 10,000 |
- Pro:Never shows an order that does not exist
- Pro:The busy state blocks a second press
- Pro:Honest words ("Confirming payment…") cover the 1-10 s wait
- Pro:Pairs naturally with one idempotency key per attempt
- Con:The rider waits the full 3.2 s at p75
- Con:Needs a clear story for timeouts (did it go through?)
A declined card or a sold-out part turns a confirmation into a retraction; Money and stock are not cheap to undo; A rider who closes the tab believes she has ordered
Nothing happens for seconds; so riders press again; Looks frozen on a slow connection
Saying it in an interview
Trade-offs.
Every lever that shrinks perceived time borrows against something, usually honesty, layout stability or code. These are the ways it goes wrong, and the choices worth making on purpose.
Where perceived-speed tricks backfire
- Pro:The rider keeps her place and context
- Pro:No layout change until the real content is ready
- Pro:Works for every filter and sort change after the first view
- Con:Nothing to dim on a first visit
- Con:Dimmed content must be inert so taps on stale items do not go astray
Throw away content the rider was reading; Shift the layout if sized wrong; A skeleton shown for long is just a slower-looking wait
Says nothing about what is coming; Collapses the layout and then expands it; Flashes on fast requests
Writing the requirement, badly and well
| Weak | Why it fails | Stronger |
|---|---|---|
| "The app feels fast." | Nobody can fail it | "Every control shows it was pressed in the next frame and reacts within 100 ms on the reference phone." |
| "API responses under 300 ms." | Measures the server, not what the rider sees, and says nothing about the screen while waiting | "Filter results replace the dimmed list within 1 s at p75 on the reference phone over 4G." |
| "Show a spinner while loading." | Flashes on fast requests and hides the band | "Acknowledge at once; an indicator only after 300 ms, held for at least 500 ms." |
| "Checkout under 1 s." | Unreachable with a payment provider in the path; invites an optimistic hack | "Busy at once, worded progress after 1 s, outcome within 10 s at p95; never optimistic." |