to110i/perceived-performance

Make web UIs feel instant even when operations take time — skeleton screens, optimistic UI, loading states, and navigation feedback.

Compatible avec~Claude Code~Codex CLI~Cursor
npx add-skill to110i/perceived-performance

name: perceived-performance description: Make web UIs feel instant even when operations take time — skeleton screens, optimistic UI, loading states, and navigation feedback. Use when a page feels slow or unresponsive, shows a blank screen while loading data, has clicks or form submissions with no immediate feedback, page transitions feel laggy, or images are slow and hurting LCP. Also use when the user asks about loading.tsx, Suspense streaming, useOptimistic, useFormStatus, SWR, prefetching, virtual scrolling, startTransition, or auth fetching making pages feel slow.

Progressive Web UI

Perceived performance is about making the UI feel fast, not just measuring fast. A page that loads in 2s but shows a skeleton immediately feels faster than one that loads in 1.5s but shows a blank screen the whole time.

When the user shares code:

  1. Read it to understand what users currently see during loading and waiting states
  2. Identify which technique(s) apply using the decision guide below
  3. Explain what to change, why it improves perceived speed, and how to implement it with concrete code

Core Principles

1. Separate "first view" from everything else

Always distinguish:

  • What is required for the first paint
  • What can load after the shell is visible
  • What can wait for user interaction

Do not treat all data as equally urgent.

2. Structure before data

The page shell (layout, nav slots, header frame) should be sent to the browser before any data fetch completes. Users form speed impressions in the first 100–200ms. Showing structure immediately reduces perceived wait even if actual load time is identical.

3. Never let a click go unacknowledged

Every interaction (click, tap, navigation) must produce a visible response in the first frame. If the real operation takes time, show an intermediate state immediately.

4. Fix the structure, not just the loading UI

loading.tsx and skeletons improve appearance, but they don't fix over-fetching or sequential data dependencies. Always address the root cause:

  • Sequential await chains → parallelize with Promise.all
  • Single large fetch → split into first-view vs. deferred
  • Full profile fetch in nav → use a lightweight nav identity helper

5. Navigation must feel fast too

Perceived speed isn't only about initial load. Page transitions, repeated visits, and interactions must feel equally fast:

  • Prefetch routes before the user clicks
  • Show loading.tsx immediately on navigation start
  • Use SWR/cache for instant return visits

Decision Guide

SymptomTechnique
Blank screen while data loadsSkeleton screens / Suspense streaming
Button click has no immediate responseOptimistic UI / instant feedback
Page transition doesn't visually start after clickloading.tsx + prefetch
Heavy component slows initial renderDynamic import
Spinner appears on every visit to the same dataSWR stale-while-revalidate
One slow API blocks an entire pageSuspense streaming (parallel async Server Components)
Auth/profile fetch delays nav or page shellAuth layout separation (see references/auth-performance.md)
Images cause layout shift or feel slownext/image with priority / blur placeholder
Font swap causes flash of unstyled textnext/font
Form submission has no visual feedbackuseFormStatus / useActionState
Long list causes scroll jank or slow renderVirtual scrolling (TanStack Virtual / react-window)
Non-urgent state update blocks user inputstartTransition for deferred rendering

Analysis Workflow

Step 1: Identify what users see first

What renders in 0–200ms? If it's blank or a spinner, structural changes are needed before styling fixes.

Step 2: Classify data by urgency

  1. First view — must be present for the page to make sense
  2. Deferred — can stream in after the shell is visible
  3. On-demand — load only after user interaction

Step 3: Fix data dependencies

  • Sequential awaitPromise.all or separate async Server Components
  • Over-fetching on initial load → split API or paginate
  • Full profile in nav → separate nav identity helper

Step 4: Apply progressive rendering

Choose based on what's needed:

  • loading.tsx — instant navigation skeleton (whole page)
  • <Suspense> — granular streaming (per section)
  • useOptimistic — instant mutation feedback
  • dynamic() — defer heavy component bundle
  • SWR — instant return visits from cache

Step 5: Validate perceived performance

  • Does something meaningful appear immediately (< 200ms)?
  • Is the user informed during all loading states?
  • Is the UI responsive before full data arrives?
  • Does interaction produce feedback in the first frame?

Output Format

When applying this skill, respond in this structure:

## What's happening now
- [describe what users currently experience]

## Root cause
- [what in the code causes the slow feeling]

## What to change and why
- [change 1]: [why this improves perceived speed]
- [change 2]: ...

## Implementation

[code]

## Notes
- [any caveats, edge cases, or follow-up improvements]

References

For implementation details with full code examples, see:

  • references/nextjs-patterns.md — Skeleton screens, optimistic UI, Suspense streaming, prefetch, dynamic import, SWR, images, fonts, interaction feedback, useFormStatus, virtual scrolling, startTransition
  • references/auth-performance.md — Auth layout separation, nav identity helpers, request/process warm-up, fallback state design

Read the relevant reference when implementing a specific technique.


Anti-Patterns to Fix

Always detect and address:

  • Awaiting full profile/auth in top-level layout → delays every page
  • Sequential await chains for independent data → unnecessary serialization
  • loading.tsx added without fixing the underlying over-fetch
  • Rendering entire datasets on initial load → paginate or virtualize
  • No visual feedback on click until operation completes → acknowledge immediately
  • Different auth sources in nav vs. page → canonical username inconsistency
  • Form submission state managed only after server responds → use useFormStatus for instant pending state
  • Rendering all items in a long list on mount → virtualize (only render visible rows)
  • Expensive state update (filter/sort/search) blocking user input → wrap in startTransition

Skills associés