Next.js (App Router) - Deep Dive
RSC payload format, the four caches, streaming SSR, server actions internals, partial prerendering, and the migration traps.
The App Router is not just a new file convention. It is a re-architecture of how React apps run, what ships to the browser, and how data flows. This is the mental model that lets you stop fighting the framework.
React Server Components, the actual protocol
When a Server Component renders, Next.js produces a serialized format called the RSC payload. It is not HTML. It is a description of the React tree where:
- Server Component output is fully rendered (props, children, text).
- Client Component references are placeholders: "render the component at chunk X with these serialized props."
- Suspense boundaries are markers, with the fallback rendered inline and the resolved content streamed later.
The browser receives the initial HTML (server-rendered) plus the RSC payload, and React reconciles them. On subsequent navigations, only the RSC payload is fetched, not full HTML. This is why client-side navigation between server-rendered pages is fast.
The four caches
Next.js 14 had four overlapping caches and a confusing default behavior. Next.js 15 simplified the defaults but the layers are still there.
- Request memoization: dedup
fetchcalls within a single render. Lives for one request. Cannot be disabled, you just dedup. - Data cache: server-side cache of
fetchresponses, keyed by URL and options. Persists across requests and deployments (it lives on disk). Opt-in in Next.js 15 viacache: 'force-cache'. - Full route cache: the rendered HTML and RSC payload for a route, cached at build time for static routes and at runtime for dynamic.
- Router cache: client-side cache of RSC payloads in the browser, so back/forward navigation is instant.
You invalidate with revalidatePath('/posts'), revalidateTag('posts'), or per-fetch next: { revalidate: 60 }.
Streaming SSR and Suspense
Suspense boundaries let the server flush HTML in chunks. The shell renders immediately, slow data sources load in parallel, each Suspense subtree streams when its data resolves.
export default function Page() {
return (
<>
<Header />
<Suspense fallback={<Skeleton />}>
<SlowSidebar />
</Suspense>
<Suspense fallback={<Skeleton />}>
<SlowFeed />
</Suspense>
</>
);
}The browser shows the header instantly, two skeletons, and fills them in as data arrives. Time to first byte is fast, time to first contentful paint is fast, total time to interactive depends on the slowest stream.
Without Suspense, the page waits for every data source before flushing anything. This is the most common performance bug I see in App Router code.
Server Actions internals
A Server Action is a POST endpoint that Next.js generates for you. The function has a stable ID baked in at build time. The client calls it via fetch with a special header. Next.js dispatches to your function, runs it, returns the result, optionally revalidates.
"use server";
export async function deletePost(id: string) {
await db.post.delete({ where: { id } });
revalidatePath("/posts");
}From a client component:
"use client";
import { deletePost } from "./actions";
export function DeleteButton({ id }: { id: string }) {
return <button onClick={() => deletePost(id)}>Delete</button>;
}Security implications:
- Anyone who can reach your site can call any Server Action by ID. There is no automatic auth.
- You must validate inputs and authorize the caller inside the action.
- Use
next-safe-actionor similar to standardize this.
Partial Prerendering
Partial Prerendering (PPR) is the long-term vision: every page has a static shell rendered at build time, with dynamic holes that stream in at request time. You get the speed of static and the freshness of dynamic, in one route.
Mark dynamic sections with Suspense. The shell is the part outside Suspense. Next.js prerenders the shell to HTML, streams dynamic content from inside Suspense boundaries on each request.
PPR is experimental in Next.js 15. When it stabilizes, it will be the default rendering mode.
Route handlers
route.ts exports HTTP methods:
export async function GET(request: Request) {
return Response.json({ hello: "world" });
}These replace the old Pages Router API routes. They run on the edge or node runtime depending on config. By default in Next.js 15, GET handlers are not cached.
Middleware
middleware.ts at the root runs on every request before routing. It runs on the Edge runtime (V8 isolates, not Node), so no fs, no native modules, limited APIs.
Use it for: auth checks, redirects, locale rewrites, geo-blocking, A/B test bucket assignment. Do not use it for: heavy compute, database queries, anything you want to cache.
Migration traps
Migrating from Pages Router to App Router is not a refactor, it is a rewrite. The traps:
getServerSidePropsandgetStaticPropsare gone. Just await data in the component._app.tsxand_document.tsxare replaced byapp/layout.tsx. Global CSS imports move there.next/routeris replaced bynext/navigation. The API is different:useRouter().push()still exists, but norouter.events.- Image and Link components import from the same paths and mostly just work.
- Third-party libraries that use Context need to be wrapped in a client component boundary.
Production observations from yashs33244.in
- Site is fully App Router, deployed on Vercel.
- Server Components for the static content (about, projects list).
- Client Components only for interactive bits (the 3D background, theme toggle, contact form).
- Bundle size is 80 KB gzipped because most of the tree is server-rendered and ships zero JS.
- LCP is under 1.2s on 4G simulated.
When not to use App Router
- You need to deploy to a non-Node runtime without Edge support. Pages Router is more portable.
- Your team is just learning React. App Router's mental model is steeper.
- You depend heavily on a library that has not adopted the React 19 / RSC conventions yet.
Learn more
- DocsNext.js App Router DocsNext.js
- DocsNext.js CachingNext.js
- DocsReact Server Components RFCReact RFCs
- ArticleVercel: Partial PrerenderingVercel Blog
- ArticleDan Abramov: Why React Server Componentsoverreacted