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, onSubmit on 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

DimensionServer ComponentsClient Components
JS shippedZeroFull component plus dependencies
Data fetchingDirect (await db.query())Through fetch or a query lib
Hooks allowedNoneAll React hooks
Event handlersNone on the elementonClick, onChange, etc.
Browser APIsNoYes
SecretsStay on serverMust not appear
SerializationRenders to RSC payloadProps must be serializable
Children boundaryCan render Client ComponentsCannot render Server Components except through props
Bundle impactZeroAdds to client bundle
Best forData fetching, SEO, layoutInteractivity, 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 useEffect to the Server Component body. Most pages shrink by half.
  • Replace useRouter from next/router with next/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.