Skip to main content

Command Palette

Search for a command to run...

React Server and Client Components Explained

Updated
5 min read
React Server and Client Components Explained

React Server Components (RSCs) are a new component type introduced in React 18 and stabilized in React 19. Next.js picked it up right after it came out, and it has been production-ready since v13.

RSCs run exclusively on the server, generate HTML output, and the component code is not sent to the browser. Unlike the traditional Client Components that render in the browser, RSCs execute on the server. RSCs can directly access server-side resources, such as databases or file systems, without requiring a separate API layer. On the flip side, they can’t access browser APIs or hooks, which should be done in the Client Components.

Server Components vs. Client Components

Starting from React 19, to define a Client Component requires adding 'use client' at the top of the file.

If the component needs any of the items listed below, it must be a Client Component.

  • React hooks (useState, useEffect, etc.)

  • Browser APIs (window, document)

  • Event handlers (onClick, onChange)

  • Third-party libraries that use browser-only features

The default for a component, without either of these directives, is a Server Component.

Read more on When to use Server and Client Components?.

Server Functions

The corresponding directive ‘user server‘ is not meant to be the signature of RSCs but rather that of Server Functions, also known as Server Actions. They can be created in Server Components and passed as props to Client Components, or imported into Client Components.

Leaves vs. Root Strategy

One of the best practices for improving web application performance is reducing the size of the JS bundle. RSCs reduce JS bundle size by:

  • Running only on the server

  • Not being included in the client-side JavaScript bundle at all

  • Returning just HTML to the browser

JS bundle for RSCs vs. CSCs:

  • Server Components = Code executes on server → HTML sent to browser → No JS bundle cost

  • Client Components = Code sent to browser → Executes in browser → Full JS bundle cost

You may wonder about the impact of the HTML output, and in some cases, it might even be larger than the equivalent JavaScript. But there are important differences in how they affect performance:

1. Parse/Execute Cost

  • HTML: Browser parses and renders it very efficiently - it's what browsers are optimized for

  • JavaScript: Must be parsed, compiled, and executed before anything happens. This is CPU-intensive and blocks interactivity

2. When User Can Interact

  • HTML: Visible and readable immediately (for non-interactive content)

  • JS bundle: User waits for download → parse → execute → hydration before page is interactive

3. Main Thread Blocking

  • HTML: Doesn't block the main thread

  • JavaScript: Large bundles block the main thread, making the page feel sluggish

With the understanding of RSCs vs. CSCs, let’s consider these two approaches for a standard ui with a search bar:

❌ Root approach (bad):

  • Put 'use client' at the Layout level (the root)

  • All components imported into a Client Component become a Client Component

  • The entire tree ships to the browser as JavaScript

✓ Leaves approach (good):

  • Keep Layout, Header, and Sidebar as Server Components

  • Only add 'use client' to Search and Navigation (the leaves)

  • Only those small pieces ship as JavaScript

In a tree, leaves are at the edges/endpoints. In your component tree, you want to push 'use client' as far out to the edges as possible - to the smallest, most specific components that actually need client-side interactivity.

The closer to the root you place 'use client', the more of your tree becomes client-side code. The closer to the leaves, the less JavaScript you ship.

Composition Pattern vs. Rendering

The most common usecase for context is to set the theme. When you use a Context Provider, you must mark it as a Client Component:

// providers.js
'use client'  // ← Required!

import { createContext } from 'react';

export const ThemeContext = createContext();

export function ThemeProvider({ children }) {
  return (
    <ThemeContext.Provider value="dark">
      {children}
    </ThemeContext.Provider>
  );
}

Even though ThemeProvider is a Client Component, its children can still be Server Components. The Client Component boundary doesn't force everything inside it to become a Client Component—only the provider itself needs to be client-side.

// app/layout.js (Server Component by default)
import { ThemeProvider } from './providers';

export default function RootLayout({ children }) {
  return (
    <html>
      <body>
        <ThemeProvider>  {/* Client Component */}
          {children}      {/* Can still be Server Components! */}
        </ThemeProvider>
      </body>
    </html>
  );
}

Children become Client Components only if you import and render them directly inside a Client Component:

// ClientWrapper.js
'use client'

import ServerChild from './ServerChild';  // ← This import makes it client!

export default function ClientWrapper() {
  return (
    <div>
      <ServerChild />  {/* Now it's a Client Component */}
    </div>
  );
}

Strategy to Minimize Bundle Size

  1. Keep as many components as Server Components as possible

  2. Push 'use client' as far down the component tree as you can, or use the composition pattern

  3. Split interactive parts into small Client Components nested within Server Components

Of course, there is a reason for everything, and there is always a tradeoff. In some cases, keeping a component as a Client Component will be more performant. For example:

  • Virtual scrolling for huge lists

  • Interactive dashboards with lots of client-side state

  • Apps where data changes frequently