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?
answer
- interactivity first, economics after
- one-time cacheable versus per-request
- big dependency, small output wins
- budget per route, weighted by traffic
- a failing build beats good intentions
basics
~20 sInteractivity 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.
solid answer
~50 sI start from the one non-negotiable: anything needing state, event handlers or browser APIs is client code, so the question is only about the rest. For that remainder I compare two cost shapes — a client chunk is code-sized, downloaded once and cached across routes but paid again in parse and execute time on every load, while a server render costs per-request compute and an RSC payload that grows with the data. Big dependency, small output, rendered once — markdown, i18n, formatting, syntax highlighting — goes server-side. Small code re-rendering large or rapidly changing data locally stays on the client, because moving it turns every interaction into a round trip. Then I make it enforceable: budgets are set per route in proportion to that route's traffic and business value, `next build` numbers land in CI, and a regression fails the build rather than being noticed later.
go deeper
Know the starting rule: anything needing state, event handlers or browser APIs must be a Client Component, and everything else is a candidate for the server.
Be able to explain both costs — cacheable code-sized JavaScript against per-request compute and a data-sized payload — and give a case on each side rather than asserting that less JavaScript always wins.
Show that you decide with measurements at realistic data volumes and can name the routes and dependencies where the budget is actually won or lost, including the regression where work was relocated rather than removed.
Own the policy and its enforcement: per-route budgets weighted by traffic, a guarded shared baseline chunk, named exceptions with owners, and a CI check that fails on regression so the standard survives team growth.
## Frame it as economics, not ideology "Server Components mean less JavaScript" is a slogan, not a policy. The leadership question is which costs you are willing to pay where, and how you keep the answer stable as the codebase grows. There are three costs in play, and each has a different shape. **Client JavaScript** is code-sized. It transfers once, caches under an immutable hashed URL, and is shared by every route that needs it — but it is parsed, compiled and executed on the user's main thread on every load, and that is the expensive part on low-end devices. **Server rendering** is per-request compute plus an **RSC payload** sized by the rendered output. It repeats on every navigation, it is not shared across routes, and it consumes capacity you pay for. **Latency** is the third: work on the server is behind a network hop, so anything that must respond within a frame of user input cannot live there. ## The decision rules I would write down 1. **Interactivity is a hard constraint, not a tradeoff.** State, event handlers, browser APIs, animation and anything on the input path is client code. Do not attempt cleverness here. 2. **Move work whose dependency dwarfs its output.** Markdown and MDX rendering, syntax highlighting, i18n and number/date formatting, chart data preparation, sanitisation. The library is tens or hundreds of kilobytes; the emitted markup is a few. This is where the budget is actually won, and it is worth being aggressive. 3. **Keep local work local when the data is large or changes fast.** A virtualised grid that sorts, filters and paginates in the browser ships a few kilobytes once and then costs nothing per interaction. Pushing it server-side converts every column click into a request and a full re-serialization — worse on both latency and bytes. 4. **Prefer moving the output, not the value.** When a boundary is unavoidable, send the finished string or the narrow view model rather than raw records plus the code that transforms them. This shrinks both sides at once. 5. **Watch the payload as carefully as the bundle.** A team that only tracks First Load JS will happily "optimise" a route by relocating a 5,000-row render to the server and shipping a far larger payload on every navigation. ## Budget per route, not per app An app-wide number is a blunt instrument. The landing page, the signed-in dashboard and the internal admin screen have different traffic, different device profiles and different tolerance. I set a per-route budget weighted by traffic and revenue exposure, and I accept a deliberately expensive interactive route — an editor, a canvas tool — as long as it is a named exception with an owner rather than a slow drift. The shared baseline chunk needs its own ceiling. It is the one number every route pays, so a dependency that lands there is the most expensive mistake available, and it deserves an explicit approval step. ## Make it mechanical Judgment does not survive headcount. The parts I would automate: - Record per-route First Load JS from `next build` in CI and fail on a regression past budget. - Track payload transfer for the top navigations in a synthetic run, so the other half of the tradeoff is visible too. - Keep client-safe and server-only helper modules physically separated, so a heavy dependency cannot reach the client graph by accident through a shared file. - Review new third-party dependencies for which graph they will live in before they are added, not after. ## What I would resist Two failure modes at the organisational level. The first is dogma — refusing client components until interactive features are painful to build and the team routes around the rule. The second is drift — nobody owns the number, every individual addition is defensible, and the route is 900 kB a year later. The remedy for both is the same: an explicit budget, an owner, and a build that fails. ## How I would present the decision With numbers on both sides for a representative route: bytes and main-thread time removed from the client, against payload bytes and server compute added, at realistic data volumes. If a proposed move cannot be shown to win on that pair, it is a refactor for taste, and it goes to the back of the queue.
- Where would you set the line for the shared baseline chunk that every route pays?Tighter than any single route, because it is multiplied across all of them, and with an explicit approval step for anything new landing there. Practically: keep it to the framework runtime plus a small set of genuinely universal primitives, and treat any product dependency arriving in the shared chunk as a defect to investigate rather than a number to renegotiate.
- How do you handle a route that legitimately needs a lot of client JavaScript, like an editor?Make it a named exception with an owner and its own budget rather than an unspoken one. Interactive tools earn their code because the code is reused across thousands of local interactions with no network cost. What I still require is that the exception does not leak — its heavy dependencies must not reach the shared chunk or any other route.
- Your team only tracks First Load JS. What blind spot does that create?They can regress the payload half while improving the tracked metric — relocating a data-heavy render to the server shows up as a win in First Load JS while shipping far more bytes per navigation. Tracking only one currency guarantees someone eventually optimises into the other. Measure payload transfer on the key navigations too.
saying these in an interview costs you the question
- Default to server components with no cost analysis
- Treats server rendering as free because it is not JavaScript
- Sets one app-wide budget for every route
- Judges the tradeoff without measuring either side
- Relies on code review discipline instead of a CI check