Skip to main content

Async Suspense Boundaries

Source: .agents/references/coding-standard/vercel-react-best-practices/rules/async-suspense-boundaries.md

Metadata​

  • title: Strategic Suspense Boundaries
  • impact: HIGH
  • impactDescription: faster initial paint
  • tags: async, suspense, streaming, layout-shift, pages-router

Content​

This repo: Pages Router only — use client <Suspense> with lazy/next/dynamic or client-side queries. Async Server Component streaming (below) is App Router only and not used here.

Strategic Suspense Boundaries (Pages Router)​

Show shell UI immediately; load slow sections behind client Suspense or dynamic import.

Incorrect (entire page blocked on client fetch in parent):

function Page() {
const { data, isLoading } = useQuery({ queryKey: ['data'], queryFn: fetchData })

if (isLoading) return <FullPageSpinner />

return (
<div>
<Sidebar />
<Header />
<DataDisplay data={data} />
<Footer />
</div>
)
}

Correct (layout renders; slow section suspends):

import dynamic from 'next/dynamic'
import { Suspense } from 'react'

const DataDisplay = dynamic(() => import('./DataDisplay'), {
loading: () => <Skeleton />
})

function Page() {
return (
<div>
<Sidebar />
<Header />
<Suspense fallback={<Skeleton />}>
<DataDisplay />
</Suspense>
<Footer />
</div>
)
}

Or colocate TanStack Query in a child so the parent layout does not wait on isLoading for the full tree.

When NOT to use this pattern:

  • Critical data needed for layout decisions (affects positioning)
  • SEO-critical content above the fold that must be in getStaticProps / getServerSideProps
  • Small, fast queries where suspense overhead isn't worth it
  • When you want to avoid layout shift (loading → content jump)

App Router only (not used in this repo)​

Async Server Components that await inside the component tree and stream via RSC Suspense boundaries. Do not introduce this pattern unless migrating to src/app/.