Back to Next.js
Next.js
medium
mid

How does Next.js handle automatic code splitting and SSR?

Next.js code-splits per route by default — each page becomes its own chunk plus a shared vendor chunk. Dynamic imports (`next/dynamic`) split inside pages. SSR happens per request: server renders the React tree, sends HTML + a `__NEXT_DATA__` payload, browser hydrates. App Router adds RSC (server-only components, ship less JS) and streaming SSR via Suspense.

5 min read·~10 min to think through

Automatic code splitting

Next.js inspects the route map and emits one chunk per page. The build output looks roughly like:

ts
.next/static/chunks/
  pages/_app.js
  pages/index.js
  pages/dashboard.js
  framework.js          // React + ReactDOM
  main.js               // Next runtime
  webpack.js            // module loader

When you visit /dashboard, the browser fetches:

  • framework.js (cached across pages),
  • main.js, webpack.js,
  • pages/_app.js,
  • pages/dashboard.js.

Marketing page / doesn't ship dashboard.js. Each route loads only its slice.

Dynamic imports inside a page

tsx
import dynamic from "next/dynamic";
const HeavyChart = dynamic(() => import("@/components/HeavyChart"), { ssr: false });

The chart becomes its own chunk, loaded only when rendered. { ssr: false } opts out of SSR entirely (useful for browser-only libraries).

Prefetching

<Link href="/dashboard"> prefetches the dashboard's JS chunk + data when the link enters the viewport. By the time the user clicks, the chunk is already cached — feels instant.

SSR (Pages Router)

tsx
export async function getServerSideProps(ctx) {
  const data = await fetchData();
  return { props: { data } };
}

export default function Page({ data }) { ... }

On request:

  1. Server runs getServerSideProps.
  2. Server renders the React tree to HTML.
  3. Response = HTML + __NEXT_DATA__ script with serialized props.
  4. Browser parses HTML, hydrates using __NEXT_DATA__ for state continuity.

SSR (App Router)

tsx
// app/dashboard/page.tsx — React Server Component
export default async function Page() {
  const data = await fetch("/api/data", { cache: "no-store" }).then(r => r.json());
  return <Dashboard data={data} />;
}
  • Server components render server-only — no client JS shipped for them.
  • Suspense boundaries enable streaming SSR: server flushes HTML in chunks as data resolves.
  • "use client" components hydrate on the browser.

RSC vs SSR

SSRRSC
Where rendersServer, per requestServer only
Client JS shippedAll component JSOnly for client components
State + interactivityAfter hydrationClient components only
Use caseSEO + first paintReduce JS / data layer code

RSC ships less JavaScript by keeping data-fetching and rendering on the server for non-interactive parts.

Caching

App Router fetches default to deduplication + caching:

  • cache: "force-cache" — like SSG.
  • cache: "no-store" — dynamic.
  • next: { revalidate: 60 } — ISR.

Image / font / script optimization

  • <Image> — auto srcset, lazy load, AVIF/WebP.
  • <Script> — control strategy (beforeInteractive, afterInteractive, lazyOnload).
  • next/font — self-hosted, zero CLS.

Tradeoffs

ProCon
Per-route splitting works out of the boxBundle waterfalls if you don't watch chunk graphs
SSR + RSC = small client JSServer cost + cold starts on serverless
Streaming + Suspense for fast above-the-foldMore mental model to debug

Interview framing

"Next.js splits per route by default — each page becomes a chunk, sharing a vendor chunk. Dynamic imports split inside a route. <Link> prefetches the next route's chunk in the background, so navigation feels instant. SSR: Pages Router uses getServerSideProps to fetch, then renders the React tree to HTML and ships hydration data. App Router uses React Server Components — non-interactive components stay on the server and ship no client JS, while "use client" boundaries hydrate. Streaming SSR via Suspense flushes HTML in chunks. The cache modes (force-cache / no-store / revalidate) let you dial freshness per fetch."

Follow-up questions

  • Compare RSC vs SSR vs SSG.
  • How does the Link prefetch work?
  • What is streaming SSR and Suspense's role?

Common mistakes

  • Importing a heavy library at the top of every page.
  • Forgetting prefetch hurts cold-start nav.
  • Mixing server-only code into client components.

Performance considerations

  • Per-route splitting cuts initial bundle. RSC + streaming cut hydration cost. Link prefetch eliminates first-visit lag.

Edge cases

  • Server-only code (db, secrets) leaking into client bundle.
  • Hydration mismatches.
  • Static export limits with SSR.

Real-world examples

  • Vercel.com, Next.js docs, large e-commerce running on App Router.

Senior engineer discussion

Seniors discuss the RSC mental model, cache mode tradeoffs, and bundle-graph hygiene (waterfalls, vendor splits, dynamic import boundaries).

Related questions