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?
answer
- code runs where the import lives
- the output travels, not the module
- zero bytes for server-only dependencies
- HTML first, RSC payload on navigation
- one client importer undoes it
basics
~20 sNone 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 sThe 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// 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
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.
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.
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.
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