Overview
Default every component to Server Component. Add "use client" only when the component needs useState, useEffect, an event handler, or a browser API. The boundary should be as low in the tree as possible: a ProductPage is a Server Component that fetches data, an AddToCart button inside it is a Client Component. The common mistake is marking the page-level component "use client" because one descendant needs interactivity; that pulls the whole subtree to the client and loses every Server Component benefit. See react-server-components for the deeper rules.
When Server Components win
Server Components are the right pick by default. Use them for everything that does not need a hook or a browser API.
- Zero JS to the client: the markup renders on the server and ships as HTML. Bundle size drops by 30 to 70 percent on a typical app.
- Direct database and file system access:
await db.query(...)inside the component body, no API route in between. - Secrets stay on the server: API keys and tokens cannot leak to the client because the component never serializes.
- SEO and first paint: the HTML is in the response, not assembled by client JS after hydration.
- Composition: a Server Component can render a Client Component as a child without pulling itself to the client.
When Client Components win
Client Components are the right pick when the component touches browser-only APIs or holds local interactive state.
useState,useReducer,useEffect,useRef: anything that needs to run between renders or persist across renders.- Event handlers:
onClick,onChange,onSubmiton interactive elements (forms can stay server with Server Actions; see forms). - Browser APIs:
localStorage,window,navigator,IntersectionObserver,ResizeObserver. - Third-party client-only libraries: charts (Recharts, Visx), editors (Tiptap, Lexical), maps (Mapbox, MapLibre).
- Animation libraries that hook into render lifecycles: Framer Motion, GSAP.
- Anything that previously needed
useLayoutEffect.
Trade-offs at a glance
| Dimension | Server Components | Client Components |
|---|---|---|
| JS shipped | Zero | Full component plus dependencies |
| Data fetching | Direct (await db.query()) | Through fetch or a query lib |
| Hooks allowed | None | All React hooks |
| Event handlers | None on the element | onClick, onChange, etc. |
| Browser APIs | No | Yes |
| Secrets | Stay on server | Must not appear |
| Serialization | Renders to RSC payload | Props must be serializable |
| Children boundary | Can render Client Components | Cannot render Server Components except through props |
| Bundle impact | Zero | Adds to client bundle |
| Best for | Data fetching, SEO, layout | Interactivity, browser APIs |
Migration cost
Moving a Pages Router app to App Router with Server Components is the real migration. Plan it route by route.
- Mark every interactive component
"use client"first; the build will tell you what is missing. - Move data fetching from
useEffectto the Server Component body. Most pages shrink by half. - Replace
useRouterfromnext/routerwithnext/navigation. The hook signatures differ; budget review time. - Server Actions replace most API routes; delete the routes as forms migrate. See forms.
- Realistic budget for a 100-route App Router migration: four to eight engineer-weeks. The win is bundle size and DX, not raw performance for users on broadband.
Recommendation
- Default to Server Component. Force every contributor to justify adding
"use client". - A button that calls an API: Client Component for the button, Server Action for the call. See forms.
- A page with a form: Server Component for the page, Client Component for the form inputs (or use Server Actions throughout).
- A dashboard: Server Component for the shell and data; Client Components for interactive widgets only.
- An SPA without SEO needs: still default to Server Components when on Next.js App Router; the boundary discipline is the point. See nextjs.
- Avoid
"use client"at the layout level. That pulls the whole route subtree client-side.