perf hacks

React Server Components Performance

Optimize React Server Components for maximum performance in Next.js

React Server Components (RSC) run exclusively on the server, sending only the rendered HTML to the client. When used correctly, they dramatically reduce JavaScript bundle size and improve performance.

SERVER vs CLIENT COMPONENTSSERVERLayoutProductListFooterAddToCart'use client'HTMLJS BundleCLIENT (Browser)Rendered HTML (no JS needed)Interactive Island(hydrated)0 KB JSComponent JS onlyServer = 0 KB client JS | Client = Component + deps

Server vs Client Components

// Server Component (default) - no "use client"
// ✅ Runs on server, zero JS sent to client
async function ProductList() {
  const products = await db.query('SELECT * FROM products');
  return (
    <ul>
      {products.map(p => <li key={p.id}>{p.name}</li>)}
    </ul>
  );
}

// Client Component - needs "use client"
// ❌ JS bundle sent to client
'use client';
function ProductList() {
  const [products, setProducts] = useState([]);

  useEffect(() => {
    fetch('/api/products').then(r => r.json()).then(setProducts);
  }, []);

  return (/* ... */);
}

Performance Benefits

AspectServer ComponentClient Component
JS Bundle0 KBComponent + dependencies
Data FetchingDirect DB/API accessNeeds API route
HydrationNone neededRequired
Time to InteractiveFasterSlower

Composition Patterns

Server Components Wrapping Client

app/page.tsx
import { ClientCounter } from './Counter';

export default async function Page() {
  // Fetch data on server
  const initialCount = await getCount();

  // Pass to client component
  return (
    <div>
      <h1>Dashboard</h1>
      <ClientCounter initialCount={initialCount} />
    </div>
  );
}

// Counter.tsx
'use client';
export function ClientCounter({ initialCount }) {
  const [count, setCount] = useState(initialCount);
  return <button onClick={() => setCount(c => c + 1)}>{count}</button>;
}

Minimizing Client Boundaries

// ❌ Bad: Entire page is client-side
'use client';
export default function ProductPage({ params }) {
  const [product, setProduct] = useState(null);
  // Everything here is client-side...
}

// ✅ Good: Only interactive parts are client
// ProductPage.tsx (Server Component)
export default async function ProductPage({ params }) {
  const product = await getProduct(params.id);

  return (
    <div>
      {/* Server-rendered, no JS */}
      <ProductHeader product={product} />
      <ProductDescription product={product} />
      <ProductSpecs product={product} />

      {/* Only this needs JS */}
      <AddToCartButton productId={product.id} />
    </div>
  );
}

Islands Architecture

// Large server-rendered page with small client islands
export default async function ArticlePage({ params }) {
  const article = await getArticle(params.slug);

  return (
    <article>
      {/* All static - no JS */}
      <ArticleHeader title={article.title} author={article.author} />
      <ArticleContent content={article.content} />

      {/* Interactive islands */}
      <Suspense fallback={<ShareButtonSkeleton />}>
        <ShareButtons url={article.url} />
      </Suspense>

      <Suspense fallback={<CommentsSkeleton />}>
        <Comments articleId={article.id} />
      </Suspense>
    </article>
  );
}

Data Fetching Patterns

Direct Database Access

// Server Component can access DB directly
import { db } from '@/lib/db';

async function UserProfile({ userId }) {
  // No API route needed!
  const user = await db.user.findUnique({
    where: { id: userId },
    include: { posts: true },
  });

  return (
    <div>
      <h1>{user.name}</h1>
      <PostList posts={user.posts} />
    </div>
  );
}

Parallel Data Fetching

async function Dashboard() {
  // Parallel fetching - all start immediately
  const [stats, users, revenue] = await Promise.all([
    getStats(),
    getUsers(),
    getRevenue(),
  ]);

  return (
    <div className="grid grid-cols-3 gap-4">
      <StatsCard stats={stats} />
      <UsersCard users={users} />
      <RevenueCard revenue={revenue} />
    </div>
  );
}

Cached Data Fetching

import { cache } from 'react';

// Memoize expensive fetches
const getUser = cache(async (id: string) => {
  const user = await db.user.findUnique({ where: { id } });
  return user;
});

// Called multiple times, only fetches once
async function UserPage({ params }) {
  const user = await getUser(params.id);
  return (
    <>
      <UserHeader user={await getUser(params.id)} />
      <UserPosts user={await getUser(params.id)} />
    </>
  );
}

Avoiding Common Mistakes

Don't Import Client Libraries in Server Components

// ❌ Bad: Imports entire library to server
import _ from 'lodash';

async function ProductList() {
  const products = await getProducts();
  const sorted = _.sortBy(products, 'name');
  return <ul>{/* ... */}</ul>;
}

// ✅ Good: Use native methods or server-safe utilities
async function ProductList() {
  const products = await getProducts();
  const sorted = products.sort((a, b) => a.name.localeCompare(b.name));
  return <ul>{/* ... */}</ul>;
}

Don't Pass Functions to Client Components

// ❌ Bad: Functions can't be serialized
async function Page() {
  const handleClick = () => console.log('clicked');
  return <ClientButton onClick={handleClick} />;
}

// ✅ Good: Use Server Actions
async function Page() {
  async function handleClick() {
    'use server';
    console.log('clicked');
  }
  return <ClientButton onClick={handleClick} />;
}

Don't Fetch in Client When Server Can

// ❌ Bad: Unnecessary client fetch
'use client';
function UserAvatar({ userId }) {
  const [user, setUser] = useState(null);
  useEffect(() => {
    fetch(`/api/users/${userId}`).then(r => r.json()).then(setUser);
  }, [userId]);
  return user ? <img src={user.avatar} /> : null;
}

// ✅ Good: Fetch in server, pass data
// Server Component
async function UserAvatar({ userId }) {
  const user = await getUser(userId);
  return <img src={user.avatar} alt={user.name} />;
}

Server Actions for Interactivity

actions.ts
'use server';

export async function addToCart(productId: string) {
  const cart = await getCart();
  await cart.add(productId);
  revalidatePath('/cart');
}

// ProductPage.tsx (Server Component)
import { addToCart } from './actions';

export default async function ProductPage({ params }) {
  const product = await getProduct(params.id);

  return (
    <div>
      <h1>{product.name}</h1>
      <form action={addToCart.bind(null, product.id)}>
        <button type="submit">Add to Cart</button>
      </form>
    </div>
  );
}

Bundle Size Impact

ApproachClient JSWhen to Use
Server Component0 KBStatic content, data fetching
Client Component with chart library200+ KBInteractive visualizations
'use client';
import { Chart } from 'chart.js'; // Ships 200KB to client

export function RevenueChart() {
  return <Chart type="bar" data={data} />;
}
import { renderChartToSVG } from './chart-server';

async function RevenueChart() {
  const data = await getRevenueData();
  const svg = renderChartToSVG(data); // Rendered on server
  return <div dangerouslySetInnerHTML={{ __html: svg }} />;
}

Performance Checklist

Component Boundaries

Default to Server Components No "use client" means zero client JS.

Only add 'use client' when needed Required for hooks and event handlers only.

Keep client components small Extract interactive parts into separate components.

Push client boundary down the tree Move 'use client' as far down as possible.

Data Fetching

Fetch in Server Components Direct DB/API access without client round-trip.

Use parallel fetching Promise.all for independent data requests.

Implement request memoization Use React's cache() for deduplication.

Avoid waterfalls Don't await in sequence when parallel is possible.

Bundle Optimization

Avoid large client dependencies Heavy libraries should stay on server.

Use server-safe alternatives Native methods instead of lodash on server.

Check bundle analyzer Verify client bundle doesn't include server code.

Tree shake unused code Configure modularizeImports for heavy packages.

On this page