# Loading states are present for async content (skeletons, spinners)

- **Pattern:** `ab-002068` (`performance-load.rendering.loading-states`)
- **Severity:** low
- **Lifecycle:** active
- **Last modified:** 2026-04-18
- **Canonical URL:** https://auditbuffet.com/patterns/ab-002068
- **License:** CC-BY-4.0 — attribute to AuditBuffet Pattern Catalog (https://auditbuffet.com/patterns/ab-002068)

## Why it matters

Async components that render blank space while fetching data feel broken to users — they perceive the app as frozen, tap repeatedly, or abandon the page before content arrives. On 3G and flaky mobile networks the gap between navigation and first paint can stretch to several seconds, directly degrading Interaction to Next Paint and Cumulative Layout Shift (Core Web Vitals that feed Google ranking signals). Missing loading states also mask real failures: a stalled fetch looks identical to a successful empty state, so users cannot tell whether to wait or retry.

## Severity rationale

Low because the UX feels sluggish but functionality still works once data eventually arrives.

## Remediation

Wrap every async component with a loading state — either an `isLoading` guard returning a skeleton, or a `<Suspense>` boundary with a fallback. Skeleton screens that mirror the final layout reduce perceived latency more than generic spinners. Example:

```tsx
import { Suspense } from 'react'

export default function Page() {
  return (
    <Suspense fallback={<CommentSkeleton count={3} />}>
      <CommentList />
    </Suspense>
  )
}
```

## Detection

- **ID:** `loading-states`
- **Severity:** `low`
- **What to look for:** Count all components that fetch data asynchronously (using `useSWR`, `useQuery`, `fetch` in `useEffect`, `Suspense` boundaries, or equivalent). For each async component, check for loading states: skeleton screens, spinners, `Suspense` fallbacks, or `isLoading` guards. Enumerate: "X async-loading components found, Y have loading states."
- **Pass criteria:** At least 80% of components with async data fetching show loading states (skeletons, spinners, Suspense fallbacks). No more than 1 async component lacks a loading state. The UX is smooth even on 3G networks — no page goes blank while waiting for data. Report: "X of Y async components have loading states."
- **Fail criteria:** 2 or more components with async data fetching lack loading states. Content goes from blank to fully rendered with no visual transition, or the page shows a blank screen while fetching.
- **Skip (N/A) when:** No async content is loaded from the client side (0 client-side data fetching patterns detected — no `useSWR`, `useQuery`, `fetch` in `useEffect`, or `Suspense` boundaries).
- **Detail on fail:** `"2 of 5 async components lack loading states — dashboard grid and comments section show blank content until data arrives"` or `"0 of 3 Suspense boundaries have fallback components — page flashes blank during data fetch"`
- **Remediation:** Add loading states:

  ```tsx
  // Before — no loading state
  export default function Comments() {
    const { data } = useSWR('/api/comments')
    return <>{data?.map(c => <Comment {...c} />)}</>
  }

  // After — with loading state
  export default function Comments() {
    const { data, isLoading } = useSWR('/api/comments')
    if (isLoading) return <CommentSkeleton count={3} />
    return <>{data?.map(c => <Comment {...c} />)}</>
  }

  // Next.js Suspense
  export default async function Comments() {
    return (
      <Suspense fallback={<CommentSkeleton count={3} />}>
        <CommentList />
      </Suspense>
    )
  }
  ```

Taxons: user-experience

HTML version: https://auditbuffet.com/patterns/ab-002068
