RSC payload, prefetching and the client cache

Next.js · Expert · 16 min

Diagnosing slow navigations, stale UI and excessive server load requires knowing how the App Router works under the hood.

A Next.js page is rendered on the server into the RSC payload (React's Flight format): the output of Server Components, placeholders and chunk references for Client Components, and the serialised props passed to them. On a first visit the server also turns it into HTML; the browser shows the HTML, then React uses the payload to reconcile and hydrate Client Components.

On client navigations no HTML is fetched: the router requests the RSC payload for the changed segments only and merges it into the current tree, so shared layouts keep their state. A Server Action response carries both the action's return value and, if it revalidated data, a fresh payload for the current route in one round trip.

Prefetching: <Link> prefetches routes as they enter the viewport or on hover. With Partial Prefetching (the default in new apps), the router prefetches each route's App Shell (static content plus session data); prefetch={true} additionally renders cached, URL-dependent content per link at the cost of a server invocation each. Prefetched payloads live in the client cache for their stale lifetime (from cacheLife, or staleTimes config).

What makes navigation slow: dynamic routes without a loading.tsx or Suspense boundary (nothing can show until the server responds), dynamic params not covered by generateStaticParams, large payloads (props passed to Client Components are serialised into every payload) and slow networks. useLinkStatus() can show pending state on the clicked link.

Example

// Rough shape of an RSC payload line stream for a page (simplified):
// 0:["$","main",null,{"children":[["$","h1",null,{"children":"Shoes"}],
//     ["$","$L1",null,{"initialLikes":12,"postId":"42"}]]}]     <- client component slot + props
// 1:I["(app-pages-browser)/./app/like-button.tsx",["static/chunks/app/like.js"],"LikeButton"]

// app/ui/nav-link.tsx - pending feedback while a navigation is in flight
'use client'
import Link, { useLinkStatus } from 'next/link'

function Spinner() {
  const { pending } = useLinkStatus()           // true while this link's navigation loads
  return pending ? <span aria-hidden> ...</span> : null
}

export function NavLink({ href, children }: { href: string; children: React.ReactNode }) {
  return (
    <Link href={href} prefetch={false}>         {/* rarely used: skip prefetch */}
      {children}
      <Spinner />
    </Link>
  )
}

// Big props are serialised into every payload: pass ids or small DTOs, not whole records.

Remember

  • Server Components render to an RSC payload; HTML is only for the first load
  • Navigations fetch payloads for changed segments and merge them into the tree
  • Link prefetches App Shells into a client cache with a stale lifetime
  • Missing loading boundaries and large serialised props make navigations slow

Watch out for

  • Passing large objects to Client Components, bloating every RSC payload and HTML response.

Check yourself

  1. During a client-side navigation between two pages that share a layout, what does the browser request?
  2. A dynamic route has no loading.tsx and no Suspense boundaries. Why can navigation to it feel unresponsive?

Sign in to answer, see the explanations, and get this lesson back as a quick review later.

Official docs: nextjs.org/docs/app/guides/prefetching