skip to content

Client Bundle Impact

The practical payoff of server components is JavaScript you never send: markdown parsers, date libraries, and SDKs stay on the server. Interviewers ask how you'd verify that, and how the RSC payload trades off against shipped JS.

part ofNext.jsoverview, primer and where to startread it →
on this pageshow

explore

questions

4

In a Next.js App Router app, a Server Component page imports a 60 kB markdown-to-HTML library and renders the result. How much of that library's JavaScript reaches the browser bundle, and what does the browser get instead?

level: juniorimportance: must knowfreq 75%

answer

  1. code runs where the import lives
  2. the output travels, not the module
  3. zero bytes for server-only dependencies
  4. HTML first, RSC payload on navigation
  5. one client importer undoes it

basics

~20 s

None of it. A library imported only by a Server Component runs at render time on the server, so the browser downloads zero bytes of it — only the rendered output, as HTML on the first load and as an RSC payload on client navigation.

solid answer

~40 s

The markdown library never enters the client bundle. In the App Router a component is a Server Component by default, and Next bundles for the browser only the modules reachable from a `'use client'` entry point. The markdown parser runs once on the server during rendering; what crosses the network is the *result* — HTML in the streamed document on a fresh load, and the serialized RSC payload describing the rendered tree on a client-side navigation. That is the headline payoff of Server Components: parsers, date and i18n libraries, ORMs and vendor SDKs stay behind. The one caveat is that this holds only while nothing on the client side imports the same module — a single import from a client file pulls it back into the browser bundle, regardless of the server page.

code

typescript · 14 lines
typescript
// app/post/[slug]/page.tsx — Server Component (no 'use client')
import { marked } from 'marked'
import { getPost } from '@/lib/posts'

export default async function PostPage({
  params,
}: {
  params: Promise<{ slug: string }>
}) {
  const { slug } = await params
  const post = await getPost(slug)
  const html = marked.parse(post.body) // runs on the server only
  return <article dangerouslySetInnerHTML={{ __html: html as string }} />
}

go deeper

for a junior

Be ready to say plainly that a dependency imported only by a Server Component is never sent to the browser, and that what travels is the rendered output rather than the code that produced it.

for a middle

Explain the mechanism: Next builds a separate client graph rooted at 'use client' entry points, and only modules reachable from those roots are emitted as browser chunks. Mention that the payoff disappears if a client module imports the same package.

for a senior

Show that you quantify it — compare First Load JS per route before and after, and know that the saving is parse and execute time on the device, not just transfer bytes. Be able to say which dependencies in a real app are the candidates.

for a principal

Own the standard: which dependency classes are forbidden in the client graph, how the budget is enforced in CI, and how you keep a server-only library from creeping back in through a shared helper module.

## The claim being tested The interviewer is checking whether you understand that in the App Router, *where a module is imported from* decides whether its code is shipped to the browser at all. A dependency used only by Server Components contributes **zero bytes** of JavaScript to the client bundle. ## Why the bytes stay behind Next builds two graphs from your app. Everything is a Server Component by default; those modules are compiled for the server runtime only. A file that begins with the `'use client'` directive is an *entry point* into the client graph, and everything reachable from it by import is compiled into browser chunks. The markdown library in the example is reachable only from `app/post/[slug]/page.tsx`, which has no directive above it, so no client chunk ever references it. ```tsx // app/post/[slug]/page.tsx — Server Component, no 'use client' import { marked } from 'marked' // stays on the server export default async function Page({ params }) { const { slug } = await params const post = await db.post.findBySlug(slug) return <article dangerouslySetInnerHTML={{ __html: marked(post.body) }} /> } ``` The parse happens once, on the server, while rendering. The browser is handed finished markup. ## What the browser downloads instead Two different things, depending on how the user arrived: - **Fresh document load** — the server streams HTML. The article body is already in that HTML, so it paints without waiting for any JavaScript. Alongside it, Next inlines the RSC payload so React can reconcile the tree when the client runtime starts. - **Client-side navigation** — no new HTML document is fetched. Next requests the **RSC payload** for the target route: a serialized, streaming description of the rendered server tree, with placeholders naming the client components to mount and the props to give them. Either way, the transferred thing is proportional to the *rendered output*, not to the size of the dependency that produced it. A 60 kB parser that emits 3 kB of markup costs 3 kB on the wire and nothing in parse/execute time on the device. ## Why this matters more than the byte count suggests Shipped JavaScript is the most expensive kind of payload. A JS chunk must be downloaded, parsed, compiled and executed on the main thread — on a mid-range phone that is several times the cost of the same number of bytes of HTML. Removing a dependency from the client graph removes all four costs, not just the transfer. It also removes a whole class of concerns: the library's own transitive dependencies, its Node-only APIs, and any secrets or credentials the code touches, since that code is never in a file the user can read. ## The caveat that actually bites teams The rule is about the *import graph*, not about intent. If any client-side module imports the same package — a `formatDate` helper in `lib/format.ts` that a client component uses, a shared `<Markdown>` wrapper that turned out to be a client component — the bundler pulls the package into the client graph and the win evaporates. The server page importing it too changes nothing; one client importer is enough. So the honest version of the answer is: *a dependency imported only from the server graph ships nothing to the browser.* When you want to be sure, look at what `next build` reports as First Load JS for the route, before and after. ## Related things that do reach the browser Server Components are not free of client cost entirely. The React client runtime and the Next router are always shipped, and every client component you render — plus its props, serialized into the payload — adds to what travels. But the *dependencies of your server rendering* are not part of that.

  • So does the browser download nothing at all for that page?
    No — it always downloads React's client runtime and the Next router, plus the JavaScript for any client components on the page and the RSC payload describing the server-rendered tree. What it does not download is the markdown library, its transitive dependencies, or any other module reachable only from the server graph.
  • What would make that same markdown library end up in the client bundle after all?
    Any client-side import path to it. If a component marked `'use client'` imports the library directly, or imports a shared helper module that imports it, the bundler follows that edge and emits the library into client chunks. The fact that a Server Component also imports it is irrelevant — the client graph is built independently.
  • How would you confirm the library really is absent from the browser bundle?
    Run `next build` and compare the First Load JS reported for that route before and after the change; a 60 kB dependency moving off the client shows up plainly. In the browser, load the page with JavaScript disabled — the rendered markdown still appears, which tells you the parsing happened server-side.

saying these in an interview costs you the question

  • Says the library is downloaded but tree-shaken away
  • Thinks Server Components run in the browser after hydration
  • Assumes server-only imports still count toward First Load JS
  • Believes adding 'use client' anywhere is harmless for size
  • Claims the RSC payload contains the library's source

context

open as a page

Moving rendering from a Client Component into a Server Component in the Next.js App Router removes JavaScript from the bundle, but the work is not free. What does the browser download instead, and how does that cost behave differently from a JavaScript chunk?

level: middleimportance: should knowfreq 45%

basics

~20 s

The browser downloads the RSC payload: a serialized description of the rendered server tree. It scales with the data rendered and is re-fetched per navigation, whereas a hashed JavaScript chunk is downloaded once, cached long-term, and reused across every route that needs it.

open as a page

You move date formatting out of a Client Component and into a Server Component in a Next.js App Router app, but `next build` reports the same First Load JS for that route and the date library still shows up in the client chunks. What are the likely causes, and how do you track it down?

level: seniorimportance: should knowfreq 40%

basics

~20 s

Something on the client side still imports the library. The bundler follows imports, not intent: one import path from any 'use client' entry point — usually through a shared helper module — keeps the package in the browser chunks no matter what the server page does.

open as a page

You lead a Next.js App Router app with a hard client JavaScript budget. How do you decide which parts of the UI keep their JavaScript on the client and which move into Server Components, given that server rendering carries its own cost?

level: principalimportance: should knowfreq 28%

basics

~20 s

Interactivity is the only hard constraint; everything else is an economics question. Move work server-side when a large dependency produces small output, keep it client-side when small code re-renders large or fast-changing data, and set the budget per route rather than per app.

open as a page