Home/Learn/React/Perceived Performance

Perceived Performance

Intermediate
Performance

How fast an interface feels is a separate problem from how fast it is. Responding immediately, showing structure early and never moving content account for most of it.

Overview

Two applications with identical network timings can feel completely different. The one that acknowledges a click within 100ms, shows the shape of the content before the content, and never shifts what the user is reading will be described as fast, even when it is objectively slower. This is not a trick — it is aligning the interface with how people perceive delay, and it is often achievable when making the actual request faster is not. It is also a strong interview answer, because it shows you think about the user rather than only the profiler.

The Thresholds

Human perception has boundaries, and they dictate the treatment.

100ms, 1s, 10s
// < 100ms   feels instant — no feedback needed
// < 300ms   noticeable — a subtle state change suffices
// < 1s      the flow of thought holds — show a spinner or skeleton
// > 1s      attention drifts — show progress and what is happening
// > 10s     they leave — show progress, an estimate, and a way to cancel

// So: acknowledge EVERY interaction within 100ms, even if the work
// takes longer. The button changes state on click, not on response.

<button onClick={run} disabled={isRunning}>
  {isRunning ? 'Running…' : 'Run'}        // instant acknowledgement
</button>

// Optimistic updates put the result on screen immediately for
// actions that almost always succeed — a like, a toggle, a reorder.
// A perceived 0ms, with a rollback if the server disagrees.

// Prefetch on intent: hovering a link starts the request ~250ms
// before the click, which is often the entire request time.

Showing Structure Early

Progressive rendering and skeletons that mean something.

Stream in parts; show cached data immediately
// Do not wait for everything. Render each part as it is ready.
<Suspense fallback={<HeaderSkeleton />}><ProblemHeader /></Suspense>
<Suspense fallback={<BodySkeleton />}><ProblemBody /></Suspense>
<Suspense fallback={<CommentsSkeleton />}><Comments /></Suspense>

// A skeleton must match the real layout. A generic grey box that
// gets replaced by a differently shaped thing causes the shift it
// was supposed to prevent.

// Show what you already have. Navigating from a list to a detail
// page, the title and difficulty are already in the cache:
const cached = queryClient.getQueryData(['problems'])?.find(p => p.slug === slug)
<h1>{data?.title ?? cached?.title}</h1>     // instant, then fills in

// Keep stale data visible while refetching rather than clearing to
// a skeleton — the screen stays usable and updates in place.

Stability

Nothing the user is looking at should move.

Reserve space, delay spinners, avoid flashing
// Reserve space for anything that loads late
<img src={cover} width={640} height={360} alt="" />   // ratio reserved
<div className="min-h-[3rem]">{message}</div>          // for a message
// Layout shift is the most irritating perceived-performance failure:
// the user goes to tap something and it moves.

// Fonts: font-display: swap plus a metric-matched fallback stops
// the text reflowing when the webfont arrives.

// Avoid the loading flash for fast responses
const showSpinner = useDelayedFlag(isLoading, 200)
// A spinner for 80ms reads as a glitch, not as feedback.

// And do not flash between states: skeleton -> empty -> data inside
// 200ms looks broken. Hold the previous content, or hold the
// skeleton for a minimum duration once it has appeared.

Key Points to Remember

  • 1Acknowledge every interaction within 100ms even when the work takes longer
  • 2Optimistic updates and prefetch-on-hover can make an action feel instantaneous
  • 3Stream content in parts with separate Suspense boundaries rather than waiting for everything
  • 4Show cached or partial data immediately and fill in the rest, instead of clearing to a skeleton
  • 5Reserve space for late-loading content and delay spinners, because layout shift and flashing feel worse than waiting

Interview Questions

Sign in to ask Aria
1

How can an app feel faster without actually being faster?

Medium
2

Why delay showing a loading spinner?

Medium
3

What makes a skeleton screen effective rather than decorative?

Medium

Ask Aria about Perceived Performance

Your personal AI tutor — ask anything about this concept

Revision Status

Personal Notes

Sign in to save personal notes for this topic.

Discussion

Sign in to join the discussion.

Loading discussion…