1. Server Components & Client Components
Under the App Router (app/), Next.js 15 treats all components as Server Components by default: they are rendered on the server first, and their results participate in page assembly as RSC Payload.
- Server Components: Rendered on the server; can directly access databases, file systems, and internal APIs; their code is not bundled into the client-side JS bundle sent to the browser, helping reduce bundle size and protect sensitive logic.
- Client Components: Declared with
"use client"at the top of the file; can useuseState,useEffect, event handlers, and browser-dependent APIs.
The table below summarizes the capability differences (with React experience, think of Server Components as "a React tree that runs on the server by default"):
| Capability / Limitation | Server Component | Client Component |
|---|---|---|
| useState / useReducer | ❌ Not supported | ✅ Supported |
| useEffect / Browser events | ❌ Not supported | ✅ Supported |
| Direct database / file access | ✅ Supported | ❌ Should not be exposed on browser side |
| async components (top-level await) | ✅ Supported | ❌ Not supported (requires child Server components or data layer splitting) |
| Included in client bundle | ❌ Not bundled to browser by default | ✅ Will be bundled |
Compared to pure React (CRA / Vite SPA)
In traditional client-side React apps, almost all components ultimately execute in the browser. Next.js App Router changes the default rendering location to the server — only modules explicitly marked with "use client" are bundled and hydrated as "client components", which is a fundamental difference in performance and data boundaries.
💡 Key Point
Prefer Server Components; only switch a module to Client Component and add "use client" when you need interactivity (local state, events, browser APIs, certain client-only third-party libraries).
2. "use client" Directive
- Must be placed at the very top of the module (before any imports), serving as the "client boundary" declaration for that file.
- Once a file has
"use client", that file and its statically imported dependency chain are treated as client components (included in the client bundle).
Example: Counter with state
"use client";
import { useState } from "react";
export function Counter() {
const [count, setCount] = useState(0);
return (
<button type="button" onClick={() => setCount((c) => c + 1)}>
点击次数:{count}
</button>
);
}
Example: Subscribing to browser environment with useEffect
"use client";
import { useEffect, useState } from "react";
export function WindowWidth() {
const [w, setW] = useState<number | null>(null);
useEffect(() => {
const onResize = () => setW(window.innerWidth);
onResize();
window.addEventListener("resize", onResize);
return () => window.removeEventListener("resize", onResize);
}, []);
return <p className="text-sm text-gray-600">视口宽度:{w ?? "…"}</p>;
}
💡 Boundary Rules
"use client" defines the module boundary between Server and Client. Client Components cannot directly import Server Components as regular sub-modules (this would force server-side modules into the client bundle). The common approach is: compose in a Server page — pass server-rendered nodes as children or props to Client components, letting the client handle only the interactive shell.
3. Advantages of Server Components
- Smaller client JS: Many display-only components stay on the server, reducing download and parsing costs.
- Zero-distance backend resource access: Query databases, read config files, and use server-only environment variables directly in components (never send secrets to the browser).
- Automatic per-route splitting: App Router naturally organizes by route segments; Server component trees lazy-load with routes, benefiting code splitting.
Example: Fetching data directly with async in a Server Component (Next.js 15 has built-in cache semantics for fetch, covered in the ISR section below):
// app/posts/page.tsx — 默认为 Server Component,无需 "use client"
type Post = { id: string; title: string };
export default async function PostsPage() {
const res = await fetch("https://jsonplaceholder.typicode.com/posts?_limit=5", {
cache: "force-cache",
});
const posts: Post[] = await res.json();
return (
<ul className="space-y-2">
{posts.map((p) => (
<li key={p.id}>{p.title}</li>
))}
</ul>
);
}
💡 Tip
When you need to make an item in the list clickable or bookmarkable, extract "that small piece" into a Client Component instead of adding "use client" to the entire page.
4. Static Rendering (SSG) & Dynamic Rendering (SSR)
Next.js 15 automatically infers at build or request time whether a route is "static" or "dynamic". Routes that don't access request-specific APIs are more likely to be statically rendered; once a request-dependent API is used during rendering, the route falls into dynamic rendering.
- Static Rendering (SSG): Generates HTML at build time (can be combined with ISR for subsequent updates). Suitable for pages with relatively stable content that don't depend on per-request context.
- Dynamic Rendering (SSR): Rendered on the server at each request (or on-demand). Calling
cookies(),headers()in Server Components/Routes, or using request-dependentsearchParams, typically triggers dynamic behavior.
For dynamic segments (e.g., app/blog/[slug]/page.tsx), you can use generateStaticParams to pre-generate a set of static paths at build time, while remaining paths can still be dynamically rendered on demand:
// app/blog/[slug]/page.tsx
type Props = { params: Promise<{ slug: string }> };
export async function generateStaticParams() {
return [{ slug: "hello" }, { slug: "next-15" }];
}
export default async function BlogPostPage({ params }: Props) {
const { slug } = await params;
return (
<article>
<h1 className="text-2xl font-bold">文章:{slug}</h1>
<p className="text-gray-600 mt-2">构建时预生成的 slug 会走静态路径;其他 slug 仍可运行时解析。</p>
</article>
);
}
Note: In Next.js 15, route params and searchParams in page props are Promises that must be awaited inside async components before use.
5. ISR (Incremental Static Regeneration)
ISR allows static pages to automatically refresh in the background after a period of time, balancing CDN caching with content timeliness. This can be configured via fetch's next.revalidate, or by exporting revalidate from page.tsx / layout.tsx for segment-level default behavior.
Page-level: export const revalidate = 60
// app/dashboard/page.tsx
export const revalidate = 60; // 秒:该路由段默认 ISR 周期
export default async function DashboardPage() {
return <p>此段每 60 秒可在后台再生成一次(具体以部署环境缓存策略为准)。</p>;
}
Fetch-level revalidate
const res = await fetch("https://api.example.com/items", {
next: { revalidate: 120 },
});
On-demand invalidation: revalidatePath / revalidateTag
In Server Actions or Route Handlers, you can precisely refresh the cache after data changes:
import { revalidatePath, revalidateTag } from "next/cache";
export async function POST() {
revalidatePath("/posts");
revalidateTag("posts-list");
return Response.json({ ok: true });
}
Use revalidateTag with fetch(..., { next: { tags: ['posts-list'] } }) to invalidate only related data rather than the entire site.
6. Streaming & Suspense
React 18+ Streaming allows the server to send HTML in chunks. Combined with Suspense, "slow" parts don't block "fast" parts. In Next.js App Router, loading.tsx within the same route automatically wraps that segment's Suspense boundary, providing instant loading placeholders.
- loading.tsx: Shows a fallback for the corresponding route segment, enabling instant feedback and streaming replacement during navigation.
- Manual Suspense: Individually wraps slow components for finer-grained control over streaming output.
// app/streaming-demo/page.tsx
import { Suspense } from "react";
async function SlowBlock() {
await new Promise((r) => setTimeout(r, 2000));
return <p className="text-green-700">慢内容已就绪</p>;
}
export default function StreamingDemoPage() {
return (
<div className="space-y-4">
<p>首屏立即可见。</p>
<Suspense fallback={<p className="animate-pulse text-gray-500">加载中…</p>}>
<SlowBlock />
</Suspense>
</div>
);
}
💡 UX Advantage
Faster first paint: Critical UI reaches the browser first; heavy data or slow APIs can progressively render within Suspense boundaries, avoiding full-page blank loading screens.
7. Composition Patterns Best Practices
Recommended structure: Keep pages and data fetching in Server Components as much as possible; extract interactive UI (forms, dialogs, rich interactive widgets) into Client Components; pass server-rendered fragments into the client "shell" via children or props.
Diagram: Layout (Server) → InteractivePanel (Client) → with slot content as DataDisplay (Server, as children):
// app/demo/page.tsx — Server Component
import { InteractivePanel } from "@/components/InteractivePanel";
import { DataDisplay } from "@/components/DataDisplay";
export default async function DemoPage() {
const data = await Promise.resolve({ title: "服务端数据" });
return (
<InteractivePanel title="可交互面板">
<DataDisplay data={data} />
</InteractivePanel>
);
}
// components/InteractivePanel.tsx — Client
"use client";
import { useState, type ReactNode } from "react";
export function InteractivePanel({
title,
children,
}: {
title: string;
children: ReactNode;
}) {
const [open, setOpen] = useState(true);
return (
<section className="rounded border p-4">
<button type="button" onClick={() => setOpen((o) => !o)}>
{open ? "收起" : "展开"} {title}
</button>
{open ? <div className="mt-3">{children}</div> : null}
</section>
);
}
// components/DataDisplay.tsx — Server Component(无 "use client")
type Props = { data: { title: string } };
export function DataDisplay({ data }: Props) {
return <p className="text-gray-800">{data.title}</p>;
}
Here DataDisplay is first rendered on the server as RSC output, then passed as children to the client-side InteractivePanel, following the officially recommended composition pattern.
8. Chapter Summary
Server by Default
Components in App Router are Server Components by default; use "use client" only when state, events, or browser APIs are needed.
Boundaries & Composition
"use client" at file top; client modules should not directly import server modules — pass server-rendered results via children instead.
Static vs Dynamic
Next auto-determines; cookies / headers / request-dependent searchParams often trigger dynamic rendering; generateStaticParams can pre-generate static pages for dynamic routes.
ISR & Invalidation
Use revalidate, fetch(..., { next: { revalidate } }) to control incremental regeneration; use revalidatePath / revalidateTag for on-demand refresh.
Streaming
loading.tsx and Suspense enable streaming output and progressive loading, improving first paint and interaction wait times.