Skip to main content

Overview

Next.js App Router introduces a new component model that distinguishes between server and client components. This distinction is crucial for optimizing performance, reducing bundle size, and improving user experience.
Server components run on the server and are rendered before being sent to the browser. They have access to server-side resources and don’t include JavaScript in the client bundle.

Characteristics

Server-Side Rendering

  • Rendered on the server
  • HTML sent to browser
  • No JavaScript in client bundle
  • Direct database access

Performance Benefits

  • Reduced bundle size
  • Faster initial page load
  • Better SEO
  • Enhanced security

When to Use Server Components

1

Data Fetching

Fetch data from databases, APIs, or file systems
2

Static Content

Display content that doesn’t change frequently
3

Large Dependencies

Use libraries that are large or server-only
4

Sensitive Operations

Handle operations that shouldn’t be exposed to the client

Example: Server Component

// app/dashboard/page.tsx (Server Component)
import { db } from "@/lib/database";

export default async function Dashboard() {
  // Direct database access on the server
  const stats = await db.getDashboardStats();

  return (
    <div>
      <h1>Dashboard</h1>
      <div className="stats-grid">
        {stats.map((stat) => (
          <div key={stat.id} className="stat-card">
            <h3>{stat.title}</h3>
            <p>{stat.value}</p>
          </div>
        ))}
      </div>
    </div>
  );
}
Client components run in the browser and provide interactivity. They include JavaScript in the client bundle and can use browser APIs and React hooks.

Characteristics

Client-Side Rendering

  • Rendered in the browser
  • Includes JavaScript bundle
  • Hydration required
  • Interactive features

Use Cases

  • Event handlers
  • State management
  • Browser APIs
  • Real-time updates

When to Use Client Components

1

Interactivity

Components that need event handlers (onClick, onChange)
2

Browser APIs

Access to localStorage, geolocation, camera, etc.
3

State Management

Components using useState, useEffect, or other hooks
4

Third-Party Libraries

Libraries that require browser APIs

Example: Client Component

"use client";

import { useState, useEffect } from "react";

export default function InteractiveCounter() {
  const [count, setCount] = useState(0);
  const [user, setUser] = useState(null);

  useEffect(() => {
    // Access browser APIs
    const savedUser = localStorage.getItem("user");
    if (savedUser) {
      setUser(JSON.parse(savedUser));
    }
  }, []);

  const handleIncrement = () => {
    setCount(count + 1);
  };

  return (
    <div>
      <h2>Interactive Counter</h2>
      <p>Count: {count}</p>
      <button onClick={handleIncrement}>Increment</button>
      {user && <p>Welcome, {user.name}!</p>}
    </div>
  );
}

Server Component with Client Children

// Server Component (app/dashboard/page.tsx)
import { db } from "@/lib/database";
import InteractiveChart from "@/components/InteractiveChart"; // Client component

export default async function Dashboard() {
  const data = await db.getChartData();

  return (
    <div>
      <h1>Dashboard</h1>
      {/* Server-rendered content */}
      <div className="stats">
        <p>Total Users: {data.totalUsers}</p>
        <p>Active Sessions: {data.activeSessions}</p>
      </div>

      {/* Client component for interactivity */}
      <InteractiveChart data={data} />
    </div>
  );
}

Client Component with Server Children

"use client";

import { useState } from "react";
import UserProfile from "@/components/UserProfile"; // Server component

export default function UserDashboard() {
  const [selectedUserId, setSelectedUserId] = useState<string>("");

  return (
    <div>
      <div className="user-selector">
        <button onClick={() => setSelectedUserId("user1")}>
          Select User 1
        </button>
        <button onClick={() => setSelectedUserId("user2")}>
          Select User 2
        </button>
      </div>

      {/* Server component for user data */}
      {selectedUserId && <UserProfile userId={selectedUserId} />}
    </div>
  );
}

Bundle Size Optimization

Use Server Components

  • Keep static content as server components
  • Reduce JavaScript bundle size
  • Improve initial page load performance

Minimize Client Components

  • Only use ‘use client’ when necessary
  • Split large client components
  • Use dynamic imports for heavy components

Data Fetching Optimization

Server-Side Fetching

  • Fetch data in server components
  • Use database connections directly
  • Implement proper caching strategies

Client-Side Fetching

  • Use for real-time updates
  • Implement proper loading states
  • Handle errors gracefully

Form Handling

"use client";

import { useState } from "react";
import { useRouter } from "next/navigation";

export default function ContactForm() {
  const [formData, setFormData] = useState({
    name: "",
    email: "",
    message: "",
  });
  const [isSubmitting, setIsSubmitting] = useState(false);
  const router = useRouter();

  const handleSubmit = async (e: React.FormEvent) => {
    e.preventDefault();
    setIsSubmitting(true);

    try {
      const response = await fetch("/api/contact", {
        method: "POST",
        headers: { "Content-Type": "application/json" },
        body: JSON.stringify(formData),
      });

      if (response.ok) {
        router.push("/thank-you");
      }
    } catch (error) {
      console.error("Error submitting form:", error);
    } finally {
      setIsSubmitting(false);
    }
  };

  return (
    <form onSubmit={handleSubmit}>
      <input
        type="text"
        value={formData.name}
        onChange={(e) =>
          setFormData((prev) => ({ ...prev, name: e.target.value }))
        }
        placeholder="Name"
        required
      />
      <input
        type="email"
        value={formData.email}
        onChange={(e) =>
          setFormData((prev) => ({ ...prev, email: e.target.value }))
        }
        placeholder="Email"
        required
      />
      <textarea
        value={formData.message}
        onChange={(e) =>
          setFormData((prev) => ({ ...prev, message: e.target.value }))
        }
        placeholder="Message"
        required
      />
      <button type="submit" disabled={isSubmitting}>
        {isSubmitting ? "Sending..." : "Send Message"}
      </button>
    </form>
  );
}

Real-Time Updates

"use client";

import { useState, useEffect } from "react";

export default function LiveStats() {
  const [stats, setStats] = useState({
    onlineUsers: 0,
    totalRequests: 0,
  });

  useEffect(() => {
    // WebSocket connection for real-time updates
    const ws = new WebSocket("ws://localhost:8080");

    ws.onmessage = (event) => {
      const data = JSON.parse(event.data);
      setStats(data);
    };

    return () => ws.close();
  }, []);

  return (
    <div className="live-stats">
      <h3>Live Statistics</h3>
      <p>Online Users: {stats.onlineUsers}</p>
      <p>Total Requests: {stats.totalRequests}</p>
    </div>
  );
}

Component Design

1

Start with Server Components

Default to server components unless client features are needed
2

Minimize Client Boundaries

Keep client components small and focused
3

Use Composition

Compose server and client components effectively
4

Optimize Data Flow

Pass data from server to client components efficiently

Performance Considerations

Bundle Analysis

  • Monitor bundle size regularly
  • Use dynamic imports for large components
  • Split client components appropriately

Hydration Optimization

  • Minimize hydration mismatches
  • Use proper loading states
  • Optimize client component rendering
Problem: Adding ‘use client’ to components that don’t need it Solution: Start with server components and only add ‘use client’ when necessary
Problem: Trying to use server-side APIs in client components Solution: Use API routes or server actions for server-side operations
Problem: Server and client rendering different content Solution: Ensure consistent rendering between server and client
Problem: Large JavaScript bundles due to unnecessary client components Solution: Audit bundle size and optimize component architecture

From Create React App

1

Identify Client Components

Find components that use hooks, event handlers, or browser APIs
2

Add 'use client' Directive

Add the directive to components that need client-side features
3

Convert Server Components

Remove ‘use client’ from components that can be server components
4

Optimize Data Fetching

Move data fetching to server components where possible
Key Takeaway: The key to optimal Next.js performance is using server components by default and only adding client components where interactivity is required. This approach reduces bundle size and improves user experience.