Perceived speed

Make the app feel instant even when it is not: answer immediately, show the right kind of waiting, and never make the user pay for the back button.

13of 16
≈ 11Min read
5Parts

The thresholds, from research that predates the web

These come from Jakob Nielsen’s response-time limits, themselves built on work from the 1960s, and they have not moved because people have not changed. Everything below is about which side of a line to land on.

DelayWhat the user experiences
≤ 100 msInstant. The response feels caused by the tap
≤ 1 sUninterrupted. They notice the delay but stay in flow
≤ 10 sThey are waiting, and need to be told something is happening
> 10 sAttention is gone. They switch tabs — and may not come back

Acknowledge in one frame, finish later

This is optimistic UI: assume success, because success is what happens 99 times in 100, and make the hundredth case an honest, visible rollback. The rule is that a rollback must never be silent — the user has to know the thing they saw did not happen.

the shape of a responsive handler
button.addEventListener("click", async () => {
  button.disabled = true;               /* 1. instant, visible acknowledgement */
  addToList(item);                      /* 2. show the result as if it worked */

  try {
    await save(item);                   /* 3. the slow part, after the paint */
  } catch {
    removeFromList(item);               /* 4. and undo it honestly if it failed */
    showError();
  } finally {
    button.disabled = false;
  }
});

The right kind of waiting

A skeleton beats a spinner because it is honest about shape: it tells the user what is coming and reserves the space it will need. A spinner tells them only that you are busy.

And an indeterminate bar that moves is perceived as faster than a static one, even at identical durations — motion reads as progress.

Expected waitShow
Under 300 msNothing. A spinner that flashes for 200 ms makes it feel slower, not faster
300 ms – 1 sAn inline state: the button in a pending style, a subtle shimmer
1 – 5 sA skeleton that matches the real layout — it also prevents the shift when content arrives
Over 5 sReal progress: a percentage, a count, or the item currently being processed

Start before they ask

eagerness: "moderate" fires on hover or on pointer-down — about 200 ms of warning, which is enough to make a navigation feel instant. Prefetching on hover has been the trick behind "instant" sites for a decade; this is the standardised version, and it can prerender the whole page, not just fetch it.

Use it carefully: a prerender runs the page’s scripts. Do not prerender something that charges a card or counts as a view.

speculation rules: prerender the page they are probably going to
<script type="speculationrules">
{ "prerender": [{ "where": { "href_matches": "/product/*" }, "eagerness": "moderate" }] }
</script>

The back button should be free

The back/forward cache keeps the whole page — DOM, JavaScript heap, scroll position — frozen in memory, so going back is instant rather than a fresh load. It is the single biggest perceived-performance feature in modern browsers, and it is easy to accidentally disable.

The blockers: an unload handler (use pagehide), cache-control: no-store on the document, an open connection the browser will not freeze, or being opened by window.opener. Chrome and Firefox both report the reason in DevTools — check it, because most sites break it without knowing.

Practice