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/.