<R/>

Crafting digital experiences

<R/>Rofiq.Full-Stack Product Engineer
AboutServicesWorkBlogRésuméContact
AboutServicesWorkBlogRésuméContact
Let's talk

One project slot open

Got something worth building? Let's make it real.

Start a project
<R/>Rofiq RahmanFull-Stack Product Engineer

Full-stack product engineering for teams that want the thing shipped, measured and maintainable.

Explore

AboutServicesWorkBlogRésuméContact

More

RésuméPrivacy policyUses — tools, hardware and setupRSS feed

Connect

© 2026 Rofiq Rahman. Crafted with Next.js.

Back to top
All articles

UX Engineering

Your loading spinner is probably making things worse

Skeletons and spinners feel like good UX. Measured against perceived performance, most of the ones I have shipped made the page feel slower. What to use instead.

Mr. Rofiq Rahman · Sep 7, 2026 · 6 min read

Storefront product listing with a promotional banner and product grid

On this page

  1. Three cases, three answers
  2. On a statically rendered page, show the content
  3. The honest version

I used to add a skeleton to everything. It felt responsible. Then I watched session recordings of the storefront I had just rebuilt, and the skeletons were the slowest-feeling part of a very fast page.

Three cases, three answers

Under 300 ms: show nothing. A flash of skeleton that disappears before the eye settles reads as a glitch, not as progress. Below roughly 300 ms, people do not perceive a wait at all — they perceive a flicker.

300 ms to about a second: keep the old content, dim it slightly. React's useTransition is built for this. The previous result stays on screen at reduced opacity while the new one loads. Nothing jumps, nothing disappears, and the page feels continuous.

const [isPending, startTransition] = useTransition();
// ...
<div className={isPending ? "opacity-60 transition-opacity" : ""}>
  <Results items={items} />
</div>

Over a second: a skeleton that matches the real layout. Not grey blobs — a skeleton with the same number of rows, the same heights, the same column widths. A skeleton whose shape differs from the content causes a layout shift when it resolves, which is the worst of both worlds.

On a statically rendered page, show the content

This is the one that embarrassed me. If the page is prerendered, the content is already in the HTML. Wrapping it in a client component that shows a skeleton for one frame is adding an animation to a page that was instant.

The honest version

Most loading states exist because the developer could see the pause in development with a throttled connection. Check what the pause actually is in production on a real device first. Often there is nothing to cover up.

  • #ux
  • #performance
  • #react
  • #loading states
Share

Keep reading

Related articles

Lighthouse report with category scores, Core Web Vitals and a request waterfall

Performance · Oct 9, 2026 · 8 min read

A performance budget that survives the deadline

Every team agrees the site should be fast, right up until the marketing tag goes in on a Thursday. Here is the setup that makes the budget hold without anyone having to be the villain.

#performance#core web vitals#next.js
Code editor with a file tree, an open component file and a terminal below

Next.js · Sep 24, 2026 · 11 min read

Server components changed how I structure a Next.js app

After six App Router projects, the folder structure I keep arriving at — and the three mistakes I made in the first two that cost me the most time.

#next.js#react#server components
Budgeting dashboard showing envelope budgets and a transaction list

Backend · Aug 16, 2026 · 5 min read

Storing money: integers, always

A short post about the one bug I refuse to debug twice, and the three places floating-point currency still sneaks into an otherwise careful codebase.

#backend#databases#money