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.
Low because the UX feels sluggish but functionality still works once data eventually arrives.
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:
import { Suspense } from 'react'
export default function Page() {
return (
<Suspense fallback={<CommentSkeleton count={3} />}>
<CommentList />
</Suspense>
)
}
ID: performance-load.rendering.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:
// 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>
)
}