Gerson

Gerson

Passionate developer specializing in web development, cloud architecture, and system design.

TypeScriptReactNext.jsPythonFastAPISQLNode.jsAWS

Understanding React Server Components in Next.js 15

A deep dive into React Server Components (RSC) in Next.js 15. Learn how they work, when to use them, and best practices for building performant applications.

Gerson
React logo and code

React Server Components (RSC) are no longer experimental - they're production-ready and fully stable in React 19.1 and Next.js 15. Understanding how they work is essential for building modern, performant web applications.

What Are React Server Components?

React Server Components are a new type of React component that runs exclusively on the server. Instead of shipping JavaScript to the browser, the server pre-renders these components and sends the result as serialized HTML/data to the client.

Server architecture diagram
Server Components render on the server and stream HTML to the client

How RSC Works Under the Hood

The rendering work is split into chunks by individual route segments (layouts and pages). Server Components are rendered into a special data format called the React Server Component Payload (RSC Payload).

RSC Rendering Flow

1. Request arrives at server
2. Server Components execute (data fetching, etc.)
3. Components serialize to RSC Payload (binary format)
4. Payload streams to client
5. React hydrates client components only
6. Page becomes interactive

The RSC Payload is a compact binary representation of the rendered React Server Components tree. It's used by React on the client to update the browser's DOM efficiently.

Server vs Client Components

In Next.js 15, all components in the app/ directory are Server Components by default. You only need to opt into client-side rendering when you need interactivity.

Server Component (Default)

// app/products/page.tsx
// No directive needed - this is a Server Component

import { prisma } from '@/lib/prisma';

export default async function ProductsPage() {
  // Direct database access - runs on server only
  const products = await prisma.product.findMany({
    where: { published: true },
    orderBy: { createdAt: 'desc' },
  });

  return (
    <div className="grid grid-cols-3 gap-4">
      {products.map((product) => (
        <ProductCard key={product.id} product={product} />
      ))}
    </div>
  );
}

Client Component (Interactive)

'use client';

// app/components/AddToCartButton.tsx
import { useState } from 'react';

export function AddToCartButton({ productId }: { productId: string }) {
  const [isAdding, setIsAdding] = useState(false);

  const handleClick = async () => {
    setIsAdding(true);
    await addToCart(productId);
    setIsAdding(false);
  };

  return (
    <button
      onClick={handleClick}
      disabled={isAdding}
      className="bg-blue-600 text-white px-4 py-2 rounded"
    >
      {isAdding ? 'Adding...' : 'Add to Cart'}
    </button>
  );
}

Key Benefits of Server Components

1. Reduced Bundle Size

Server Component code is never shipped to the browser. A SaaS provider that adopted Next.js 15 with RSC for its analytics dashboard reduced client bundle sizes by 60% and observed a 25% drop in infrastructure costs.

2. Direct Data Access

Server Components can directly access databases, file systems, and internal services without exposing APIs:

Direct Database Access

// This runs on the server - no API route needed
async function UserProfile({ userId }: { userId: string }) {
  const user = await prisma.user.findUnique({
    where: { id: userId },
    include: { posts: true, followers: true },
  });

  return (
    <div>
      <h1>{user.name}</h1>
      <p>{user.followers.length} followers</p>
    </div>
  );
}

3. Enhanced Security

Sensitive data and logic stay on the server. API keys, database credentials, and business logic are never exposed to the client.

Best Practice: Keep most of your UI in Server Components. Use Client Components only where interactivity is needed (forms, buttons, animations).

Common Patterns

Composition Pattern

Server Components can render Client Components, but not vice versa. Use composition to mix both:

Composition Example

// Server Component
import { ClientSidebar } from './ClientSidebar';

export default async function Dashboard() {
  const data = await fetchDashboardData();

  return (
    <div className="flex">
      <ClientSidebar />  {/* Interactive sidebar */}
      <main>
        {/* Server-rendered content */}
        <DashboardStats stats={data.stats} />
        <RecentActivity items={data.activity} />
      </main>
    </div>
  );
}

Passing Server Data to Client Components

Props passed to Client Components must be serializable:

Serializable Props

// Server Component
export default async function Page() {
  const products = await getProducts();

  // Pass serializable data to client component
  return <ProductFilter products={products} />;
}

// Client Component
'use client';
export function ProductFilter({ products }: { products: Product[] }) {
  const [filter, setFilter] = useState('');
  // Interactive filtering logic...
}

Avoid: Don't wrap entire pages in "use client". It defeats the purpose of RSC and ships unnecessary JavaScript to the browser.

Performance Impact

The performance benefits are significant:

  • Faster Initial Load - Users see content immediately without waiting for JS to download and hydrate
  • Lower Memory Usage - Less JavaScript means less memory consumption on client devices
  • Better SEO - Fully rendered HTML is sent to search engines
  • Reduced Data Transfer - Only necessary client code is sent to the browser

Conclusion

React Server Components represent a fundamental shift in how we build React applications. By rendering on the server by default and only hydrating interactive parts, we get the best of both worlds: the component model of React with the performance of server-rendered HTML.

Start by defaulting to Server Components and only add "use client" when you need browser APIs or interactivity. Your users (and your hosting bill) will thank you.