skip to content

Server Components

Server Components run only on the server and never ship their code to the browser, which quietly changes how you fetch data and how big your bundle gets. Interviewers probe the 'use client' boundary because that one line decides most of it.

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

explore

questions

20

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

In the Next.js App Router, `app/orders/page.tsx` queries the database directly and awaits the result. Which pieces of the classic browser-fetch setup does that remove, and what does the browser receive instead?

level: juniorimportance: must knowfreq 76%

basics

~20 s

app/orders/page.tsx runs only on the server, so it awaits the query and renders the rows itself. The API route, the browser fetch, and the useEffect loading and error state all disappear. The browser receives rendered markup plus the RSC payload, never the query.

open as a page

In the Next.js App Router, files under app/ render on the server unless told otherwise. Which of your components actually need a 'use client' directive, and which do not?

level: juniorimportance: must knowfreq 80%

basics

~20 s

Only components that need browser behaviour: React state or effect hooks, DOM event handler props such as onClick, or browser APIs like window and localStorage. Everything else — markup, layout, data fetching — stays a Server Component with no directive.

open as a page

In a Next.js App Router app you add a client-side ThemeProvider by wrapping {children} with it inside app/layout.tsx. Does that turn every page and component below it into client code? Explain what actually happens.

level: middleimportance: must knowfreq 72%

basics

~20 s

No. Only the provider module and its own imports become client code. The pages arrive as the layout's children prop — created by the Server Component layout, rendered on the server, and handed to the provider as an already-rendered slot.

open as a page

In a Next.js App Router page, a Server Component loads a user row with 30 columns, renders the name as text, and passes only { id, name } into a Client Component. Which of that data actually reaches the browser, and how would you verify it?

level: middleimportance: must knowfreq 60%

basics

~20 s

Two things reach the browser: the markup the Server Component rendered, and a verbatim copy of the props handed to the Client Component. The other 28 columns stay on the server. Verify by searching the page source for a value you never passed.

open as a page

A Next.js App Router product page has 'use client' at the top of page.tsx purely so an Add to Cart button can handle a click, and product data fetched higher up is drilled down through three client components to reach it. Walk through how you restructure this.

level: seniorimportance: must knowfreq 58%

basics

~20 s

Move the directive down to the button. Keep page.tsx a Server Component that fetches where the data is used, render the interactive leaf inline, and where a client component must wrap content, give it a children prop instead of hoisting the boundary above the whole subtree.

open as a page

In a Next.js App Router app, a client component <FilterBar> renders another client component <Select onChange={handleChange} /> and passes it a function prop. Why does that not produce the "only plain objects can be passed to Client Components" error that the same prop would cause when passed from a page.tsx server component?

level: juniorimportance: should knowfreq 45%

basics

~20 s

Serialization happens only where a value crosses from the server render into the RSC payload. FilterBar and Select both run in the browser, so the function is handed over as an in-memory reference and is never written to the wire.

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 add 'use client' to app/dashboard/layout.tsx so it can hold a sidebar-open flag in useState. Does app/dashboard/page.tsx nested inside it become a Client Component too, and what else changes for that layout file?

level: middleimportance: should knowfreq 48%

basics

~20 s

The nested page stays a Server Component. Next imports layout and page as separate modules and passes the rendered page in as children, so the directive never reaches it. The layout file itself loses server-only abilities: no async data, no metadata export.

open as a page

In a Next.js App Router app, is `children` the only prop through which server-rendered content can reach a Client Component, and what is the rule that makes it work?

level: middleimportance: should knowfreq 42%

basics

~20 s

Any prop can carry it. children is just the conventional name — a Server Component can pass JSX through named props like header or sidebar too. The rule is that the element must be created by the server parent, not imported by the client component.

open as a page

A Next.js App Router codebase has a `lib/billing.ts` module that reads an API key from `process.env` and calls a paid vendor. What does adding `import 'server-only'` at the top of that file buy you that a comment saying "server only" does not?

level: middleimportance: should knowfreq 46%

basics

~20 s

Importing the server-only package turns a convention into a build failure. If any client module ever imports that file, directly or through a chain of imports, the build breaks and names the offending file instead of silently shipping the module to the browser.

open as a page

In a Next.js App Router app, a component marked 'use client' reads process.env.API_TOKEN and logs undefined in the browser, while the identical read works inside a Server Component. Why, and what should the team do about it?

level: middleimportance: should knowfreq 52%

basics

~20 s

Next only inlines environment variables whose names start with NEXT_PUBLIC_ into browser code, as a build-time text substitution. Anything else is simply absent in the browser and reads as undefined. If the value is a secret, keep the work on the server and pass only the result.

open as a page

You import a UI component from an npm package into a Next.js App Router page and the build fails saying the component needs useState and only works in a Client Component — but the package ships no 'use client' directive of its own. How do you fix it without turning the page into a Client Component?

level: middleimportance: should knowfreq 48%

basics

~20 s

Create a thin module of your own that starts with 'use client' and re-exports the package's component, then import that module from the page. Your wrapper becomes the boundary, the package's code sits below it, and the page stays a Server Component.

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

A Next.js App Router page passes a 5,000-row array from a Server Component into a client-rendered table, and the HTML document comes back around 2 MB — roughly twice the size of the JSON itself. Why does that data appear twice in the response, and what would you change?

level: seniorimportance: should knowfreq 42%

basics

~20 s

The rows ship twice: once as the table markup produced by server-rendering the client component, and once as its serialized props inlined in the same document so hydration can rebuild the tree without refetching. Send fewer rows and fewer fields.

open as a page

A Next.js App Router page renders in about 1.8 seconds. Its server component awaits `getUser()`, then `getOrders()`, then `getRecommendations()`, and none of the three depends on the others. Why is the page slow, and how do you fix it?

level: seniorimportance: should knowfreq 56%

basics

~20 s

Three independent awaits in sequence cost the sum of their latencies, not the maximum. Start all three calls before awaiting any of them and combine with Promise.all, so the page waits roughly as long as the slowest one instead of all three added together.

open as a page

In a Next.js App Router app, a team adds 'use client' to the top of app/dashboard/layout.tsx. Which code actually crosses into the client boundary as a result, and do the pages rendered inside that layout become Client Components too?

level: seniorimportance: should knowfreq 42%

basics

~20 s

The boundary follows the import graph, not the route tree. The layout module and everything it imports become client code; the pages nested under it do not, because Next renders them separately and passes their output in as the children prop.

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

Your Next.js App Router root layout has accumulated six client providers wrapping {children} — theme, a query client, analytics, feature flags, toasts and i18n. As the engineer who owns this app, how do you decide which of them stay at the root and which move down?

level: principalimportance: should knowfreq 32%

basics

~20 s

Judge each provider by who consumes it. A provider must sit above its consumers, so anything used by one section belongs in that section's layout, not the root. Root placement costs every route that JavaScript, and some values need no provider at all.

open as a page

In a Next.js App Router codebase, any server component can query the database directly. As the engineer setting the standard, how do you decide between colocating queries in the components that need them and routing every read through a shared data access layer?

level: principalimportance: nice to knowfreq 32%

basics

~20 s

Colocate the call, centralise the query. Components should ask for what they need from named functions in a server-only data access module, so authorization, query shape and instrumentation live in one place while each component still fetches independently.

open as a page