We ship milliseconds, but nobody experiences milliseconds. People experience whether a thing felt instant, whether a button felt like it heard them, whether the screen kept up with their thumb.
That gap, between fast and feels-fast, is where most of the work lives.
The dashboard lies a little
A p95 of 120ms looks great in a chart. But a chart averages away the moment that actually matters: the first tap, on a cold cache, on a tired phone, on hotel wifi. That one is the impression people keep.
So I stopped optimizing the average and started hunting the worst honest case:
// not "is it fast", but "when is it slowest, and who feels it"
const worst = samples.sort((a, b) => b.ms - a.ms).slice(0, 10) Feel is mostly about honesty
A spinner that shows up after 100ms reads as “working.” The same spinner at 800ms reads as “broken.” The number barely moved. The feeling fell off a cliff.
Optimistic UI isn’t a trick to look faster. It’s a promise you intend to keep.
The fix is rarely a faster server. It’s usually telling the truth sooner: an optimistic update, a skeleton that matches the real layout, a transition that absorbs the wait instead of hiding it.
What I do now
- Measure the cold, unlucky path, not the warm one.
- Respond to input within a frame, even if the data isn’t ready.
- Make waiting feel intentional, never frozen.
Fast is a number. Feels-fast is a craft. I care about the second one.