| title | Next.js 全指南:从上手到进阶,以及不该用的场景 | ||
|---|---|---|---|
| date | 2025-11-10 | ||
| tags |
|
||
| category | 前端工程 | ||
| summary | 系统梳理 Next.js 的核心能力与使用边界。从路由、渲染模式、数据获取等基础用法,到中间件、并行路由、流式渲染等进阶特性,最终给出明确的选型建议:什么时候该用 Next.js,什么时候不该用。 |
现代前端项目面临的核心矛盾:用户需要极致的首屏体验(快、SEO 友好),开发者需要高效的全栈开发体验(路由、数据获取、部署一体化)。
具体痛点:
- 单页应用(SPA)首屏白屏时间长,搜索引擎无法抓取动态内容,SEO 表现差
- 传统 SSR 方案(Express + React 手动拼接)需要自己处理路由、代码分割、数据预取,工程复杂度高
- 静态站点生成(SSG)适合内容不变的页面,但数据频繁变化时需要重新构建,灵活性不足
- 全栈项目需要前后端分离部署,API 跨域、鉴权、部署流程繁琐
为什么选择 Next.js 而不是其他方案:
- 对比 CRA / Vite SPA:Next.js 提供内置 SSR/SSG,无需手动配置服务端渲染
- 对比 Remix:Next.js 生态更成熟,社区资源更丰富,App Router 的设计理念更贴近 React Server Components 的演进方向
- 对比 Nuxt(Vue):如果你团队技术栈是 React,Next.js 是唯一的全栈框架选择
- 对比手搓 SSR:Next.js 内置路由、代码分割、预渲染、图片优化、中间件等,开箱即用
但也存在误用风险:
- 简单的内部管理后台强行使用 Next.js,引入不必要的 SSR 复杂度
- 纯静态展示页用 Next.js,构建产物体积远大于纯静态方案
- 不理解 Server Components 和 Client Components 的边界,导致性能反而变差
本文将系统梳理 Next.js 的核心用法、进阶特性、适用场景以及明确的使用边界。
# 创建项目
npx create-next-app@latest my-app
# 推荐选项:
# ✔ TypeScript: Yes
# ✔ ESLint: Yes
# ✔ Tailwind CSS: Yes
# ✔ src/ directory: Yes
# ✔ App Router: Yes
# ✔ Import alias: @/*项目结构(App Router):
my-app/
├── app/
│ ├── layout.tsx # 根布局(必须)
│ ├── page.tsx # 首页
│ ├── loading.tsx # 全局 loading 状态
│ ├── error.tsx # 全局 error 边界
│ ├── not-found.tsx # 404 页面
│ ├── about/
│ │ └── page.tsx # /about 路由
│ └── blog/
│ ├── page.tsx # /blog 列表页
│ └── [slug]/
│ └── page.tsx # /blog/:slug 动态路由
├── components/ # 公共组件
├── lib/ # 工具函数、配置
├── public/ # 静态资源
└── middleware.ts # 中间件(可选)
App Router 基于文件系统路由,app/ 目录结构即路由结构:
基本路由:
// app/about/page.tsx → 访问 /about
export default function AboutPage() {
return <h1>关于我们</h1>;
}动态路由:
// app/blog/[slug]/page.tsx → 访问 /blog/hello-world
interface Props {
params: Promise<{ slug: string }>; // Next.js 15+ params 是 Promise
}
export default async function BlogPost({ params }: Props) {
const { slug } = await params;
const post = await getPost(slug);
return <article>{post.title}</article>;
}
// 生成静态路径(SSG 时使用)
export async function generateStaticParams() {
const posts = await getAllPosts();
return posts.map((post) => ({ slug: post.slug }));
}路由组(Route Groups):
// (marketing)/about/page.tsx → /about
// (dashboard)/settings/page.tsx → /settings
// 括号目录不出现在 URL 中,用于共享布局布局系统:
// app/layout.tsx — 根布局,所有页面共享
export default function RootLayout({ children }: { children: React.ReactNode }) {
return (
<html lang="zh-CN">
<body>
<nav>全局导航栏</nav>
{children}
</body>
</html>
);
}
// app/dashboard/layout.tsx — dashboard 布局
export default function DashboardLayout({ children }: { children: React.ReactNode }) {
return (
<div className="flex">
<aside>侧边栏</aside>
<main>{children}</main>
</div>
);
}模板 vs 布局:
layout.tsx:跨路由导航时保持状态(不会重新渲染)template.tsx:每次导航都重新创建实例(适合需要重置状态的场景)
Next.js 支持四种渲染模式,通过不同的函数和配置切换:
① 服务端渲染(SSR) — 每次请求时生成 HTML:
// app/dashboard/page.tsx
// 默认就是 Server Component,每次请求都会在服务端渲染
export default async function DashboardPage() {
const data = await fetch('https://api.example.com/stats', {
cache: 'no-store', // 不缓存,每次请求都获取最新数据
});
const stats = await data.json();
return <Dashboard stats={stats} />;
}② 静态站点生成(SSG) — 构建时生成 HTML:
// app/blog/page.tsx
// fetch 默认会缓存(Next.js 的行为),构建时获取数据
export default async function BlogPage() {
const posts = await fetch('https://api.example.com/posts');
const data = await posts.json();
return <PostList posts={data} />;
}③ 增量静态再生(ISR) — 静态页面定时更新:
// app/blog/[slug]/page.tsx
export default async function BlogPost({ params }: Props) {
const { slug } = await params;
const post = await fetch(`https://api.example.com/posts/${slug}`, {
next: { revalidate: 3600 }, // 每 3600 秒重新验证一次
});
const data = await post.json();
return <Article data={data} />;
}④ 客户端渲染(CSR) — 浏览器中渲染:
'use client'; // 声明为客户端组件
import { useState, useEffect } from 'react';
export default function ClientOnly() {
const [data, setData] = useState(null);
useEffect(() => {
fetch('/api/data').then(r => r.json()).then(setData);
}, []);
if (!data) return <div>加载中...</div>;
return <div>{data.message}</div>;
}四种模式对比:
| 模式 | 渲染时机 | 数据新鲜度 | 服务器压力 | SEO | 适用场景 |
|---|---|---|---|---|---|
| SSR | 每次请求 | 最新 | 高 | 好 | 个性化页面、实时数据 |
| SSG | 构建时 | 构建时 | 最低 | 好 | 博客、文档、营销页 |
| ISR | 构建时+定时更新 | 可控 | 低 | 好 | 内容更新不频繁的页面 |
| CSR | 浏览器 | 最新 | 无 | 差 | 管理后台、交互密集页 |
Server Components 中直接 fetch:
// Server Component — 默认
async function UserProfile({ userId }: { userId: string }) {
const res = await fetch(`https://api.example.com/users/${userId}`, {
next: { tags: ['user'] }, // 按需重新验证的 tag
});
const user = await res.json();
return <div>{user.name}</div>;
}Client Components 中使用 SWR / React Query:
'use client';
import useSWR from 'swr';
const fetcher = (url: string) => fetch(url).then(r => r.json());
function ClientProfile({ userId }: { userId: string }) {
const { data, error, isLoading } = useSWR(
`/api/users/${userId}`,
fetcher,
{ refreshInterval: 30000 } // 30 秒自动刷新
);
if (error) return <div>加载失败</div>;
if (isLoading) return <div>加载中...</div>;
return <div>{data.name}</div>;
}Route Handlers(API 路由):
// app/api/users/route.ts → GET /api/users
import { NextResponse } from 'next/server';
export async function GET(request: Request) {
const { searchParams } = new URL(request.url);
const page = searchParams.get('page') || '1';
const users = await db.user.findMany({
skip: (Number(page) - 1) * 10,
take: 10,
});
return NextResponse.json({ users, page: Number(page) });
}
export async function POST(request: Request) {
const body = await request.json();
const user = await db.user.create({ data: body });
return NextResponse.json(user, { status: 201 });
}这是 Next.js App Router 最核心的概念,也是最容易混淆的:
Server Component(默认)
├── 在服务端执行,不发送 JS 到浏览器
├── 可以直接访问数据库、文件系统
├── 不能使用 useState、useEffect、事件处理
└── 不能使用浏览器 API(window、document)
Client Component('use client')
├── 在浏览器中执行(SSR 时也会预渲染)
├── 可以使用 React Hooks、事件处理
├── 可以访问浏览器 API
└── 不能直接访问数据库、文件系统
组件划分策略:
// ✅ Server Component — 数据获取层
async function PostPage() {
const posts = await fetchPosts(); // 服务端直接查数据库
return <PostList posts={posts} />;
}
// ✅ Client Component — 交互层
'use client';
function PostList({ posts }: { posts: Post[] }) {
const [filter, setFilter] = useState('');
const filtered = posts.filter(p => p.title.includes(filter));
return (
<div>
<input value={filter} onChange={e => setFilter(e.target.value)} />
{filtered.map(post => <PostCard key={post.id} post={post} />)}
</div>
);
}关键原则:尽量让组件保持为 Server Component,只在需要交互时才标记 'use client'。
Middleware 在请求到达页面之前执行,适合鉴权、重定向、日志等:
// middleware.ts(项目根目录)
import { NextResponse } from 'next/server';
import type { NextRequest } from 'next/server';
export function middleware(request: NextRequest) {
// 检查登录状态
const token = request.cookies.get('auth-token')?.value;
if (!token && request.nextUrl.pathname.startsWith('/dashboard')) {
return NextResponse.redirect(new URL('/login', request.url));
}
// 地域重定向
const country = request.geo?.country || 'CN';
if (country !== 'CN' && request.nextUrl.pathname === '/') {
return NextResponse.redirect(new URL('/en', request.url));
}
// 添加自定义请求头
const response = NextResponse.next();
response.headers.set('x-request-id', crypto.randomUUID());
return response;
}
// 配置匹配规则
export const config = {
matcher: [
'/dashboard/:path*', // 匹配 /dashboard 及其子路径
'/api/:path*', // 匹配所有 API 路由
'/((?!_next/static|_next/image|favicon.ico).*)', // 排除静态资源
],
};注意事项:
- Middleware 运行在 Edge Runtime,不能使用 Node.js API(如
fs、path) - 执行时间有限制(Vercel 免费版 5 秒),不适合耗时操作
- 不要在 Middleware 中做大量计算
并行路由(Parallel Routes)— 同一布局内同时渲染多个页面:
// app/dashboard/@analytics/page.tsx
// app/dashboard/@team/page.tsx
// app/dashboard/layout.tsx
export default function DashboardLayout({
children,
analytics,
team,
}: {
children: React.ReactNode;
analytics: React.ReactNode;
team: React.ReactNode;
}) {
return (
<div className="grid grid-cols-3">
<div>{children}</div>
<div>{analytics}</div>
<div>{team}</div>
</div>
);
}拦截路由(Intercepting Routes)— 在当前布局内打开其他路由:
// 结构:
// app/feed/page.tsx → 正常访问 /feed
// app/@modal/(.)photo/[id]/page.tsx → 从 /feed 点击时在弹层中打开
// app/photo/[id]/page.tsx → 直接访问 /photo/:id 时全屏展示
// app/@modal/default.tsx
export default function Default() {
return null; // 没有拦截时,modal 插槽为空
}
// app/layout.tsx
export default function Layout({
children,
modal,
}: {
children: React.ReactNode;
modal: React.ReactNode;
}) {
return (
<>
{children}
{modal} {/* 拦截路由的内容渲染在这里 */}
</>
);
}适用场景:
- 社交平台的图片/文章详情弹窗(Instagram 风格)
- 不离开当前页面的快速预览
- 需要独立 URL 但不想丢失当前页面状态
// app/page.tsx
import { Suspense } from 'react';
export default function Page() {
return (
<div>
<h1>首页</h1>
{/* 快速渲染的部分 */}
<StaticHeader />
{/* 慢数据 — 流式渲染,不阻塞整个页面 */}
<Suspense fallback={<Skeleton />}>
<SlowDataComponent />
</Suspense>
<Suspense fallback={<Skeleton />}>
<AnotherSlowComponent />
</Suspense>
</div>
);
}
// 慢组件 — 数据获取可能需要数秒
async function SlowDataComponent() {
const data = await fetch('https://slow-api.example.com/data', {
cache: 'no-store',
});
const result = await data.json();
return <DataView data={result} />;
}流式渲染的优势:
- 页面可以分块传输,用户先看到已就绪的内容
- 某个组件的数据请求失败不影响其他组件
- 结合 Suspense,体验优于传统 SSR 的全页等待
Server Actions 允许客户端组件直接调用服务端函数,无需手写 API:
// app/actions.ts
'use server';
import { revalidatePath } from 'next/cache';
import { redirect } from 'next/navigation';
export async function createPost(formData: FormData) {
const title = formData.get('title') as string;
const content = formData.get('content') as string;
// 直接操作数据库
await db.post.create({
data: { title, content, authorId: 'current-user' },
});
// 重新验证缓存
revalidatePath('/blog');
redirect('/blog');
}
export async function deletePost(id: string) {
await db.post.delete({ where: { id } });
revalidatePath('/blog');
// 不需要 redirect,返回即可
}// app/blog/new/page.tsx
import { createPost } from '../actions';
export default function NewPostPage() {
return (
<form action={createPost}>
<input name="title" placeholder="标题" required />
<textarea name="content" placeholder="内容" required />
<button type="submit">发布</button>
</form>
);
}
// 在客户端组件中使用
'use client';
import { deletePost } from '../actions';
import { useTransition } from 'react';
function DeleteButton({ id }: { id: string }) {
const [isPending, startTransition] = useTransition();
return (
<button
disabled={isPending}
onClick={() => startTransition(() => deletePost(id))}
>
{isPending ? '删除中...' : '删除'}
</button>
);
}安全注意事项:
- Server Actions 本质是 POST 请求,需要验证用户权限
- 对
formData的输入做校验(推荐用 Zod) - 不要信任客户端传来的任何数据
import Image from 'next/image';
import { Inter } from 'next/font/google';
// 字体优化 — 自动子集化,避免布局抖动
const inter = Inter({ subsets: ['latin'] });
export default function Page() {
return (
<main className={inter.className}>
{/* 图片优化 — 自动格式转换、尺寸适配、懒加载 */}
<Image
src="/hero.jpg"
alt="首页横幅"
width={1200}
height={600}
priority // 首屏图片优先加载
placeholder="blur" // 模糊占位
/>
{/* 远程图片需要配置域名 */}
<Image
src="https://cdn.example.com/photo.jpg"
alt="远程图片"
width={800}
height={600}
sizes="(max-width: 768px) 100vw, 50vw"
/>
</main>
);
}// next.config.ts — 配置远程图片域名
const nextConfig = {
images: {
remotePatterns: [
{ protocol: 'https', hostname: 'cdn.example.com' },
],
},
};// app/[lang]/layout.tsx
import { dictionaries } from '@/lib/i18n';
interface Props {
params: Promise<{ lang: string }>;
}
export default async function LangLayout({ children, params }: Props & { children: React.ReactNode }) {
const { lang } = await params;
const dict = await dictionaries[lang]();
return (
<html lang={lang}>
<body>
<nav>
<a href={`/${lang}`}>{dict.nav.home}</a>
<a href={`/${lang}/about`}>{dict.nav.about}</a>
</nav>
{children}
</body>
</html>
);
}// middleware.ts — 语言检测与重定向
import { NextResponse } from 'next/server';
import type { NextRequest } from 'next/server';
const locales = ['zh', 'en'];
export function middleware(request: NextRequest) {
const { pathname } = request.nextUrl;
const pathnameHasLocale = locales.some(
locale => pathname.startsWith(`/${locale}/`) || pathname === `/${locale}`
);
if (pathnameHasLocale) return;
// 根据 Accept-Language 头部重定向
const locale = request.headers.get('accept-language')?.startsWith('zh') ? 'zh' : 'en';
request.nextUrl.pathname = `/${locale}${pathname}`;
return NextResponse.redirect(request.nextUrl);
}Next.js 有多级缓存,理解它们是性能优化的关键:
请求级缓存(fetch 的 cache 选项)
├── force-cache → 缓存直到手动失效(SSG 默认)
├── no-store → 不缓存(SSR 默认)
└── revalidate: N → N 秒后重新验证(ISR)
路由级缓存
├── React Cache → 单次请求内的组件缓存(fetch dedup)
├── Full Route Cache → 构建时缓存整个路由的渲染结果
└── Router Cache → 客户端路由缓存(浏览器内存)
按需重新验证:
// 使用 tags 实现精确缓存失效
async function getPost(slug: string) {
const res = await fetch(`https://api.example.com/posts/${slug}`, {
next: { tags: [`post-${slug}`] },
});
return res.json();
}
// 在 Server Action 或 Route Handler 中触发失效
'use server';
import { revalidateTag } from 'next/cache';
export async function updatePost(slug: string, data: PostData) {
await db.post.update({ where: { slug }, data });
revalidateTag(`post-${slug}`); // 只失效这一篇文章的缓存
}# 安装 bundle 分析工具
pnpm add -D @next/bundle-analyzer// next.config.ts
import analyze from '@next/bundle-analyzer';
const withBundleAnalyzer = analyze({
enabled: process.env.ANALYZE === 'true',
});
export default withBundleAnalyzer({
// 其他配置
});# 分析
ANALYZE=true pnpm build减少客户端 JS 体积的技巧:
// ❌ 整个库被打包到客户端
'use client';
import { marked } from 'marked'; // 40KB+
// ✅ 在 Server Component 中处理
async function MarkdownContent({ content }: { content: string }) {
const html = marked(content); // 服务端执行,不增加客户端体积
return <div dangerouslySetInnerHTML={{ __html: html }} />;
}
// ✅ 动态导入大型客户端库
'use client';
import dynamic from 'next/dynamic';
const HeavyChart = dynamic(() => import('./HeavyChart'), {
loading: () => <Skeleton />,
ssr: false, // 不需要 SSR
});// app/blog/[slug]/page.tsx
import type { Metadata, ResolvingMetadata } from 'next';
interface Props {
params: Promise<{ slug: string }>;
}
// 静态 metadata
export const metadata: Metadata = {
title: '博客',
description: '技术博客',
};
// 动态 metadata(根据数据生成)
export async function generateMetadata(
{ params }: Props,
parent: ResolvingMetadata
): Promise<Metadata> {
const { slug } = await params;
const post = await getPost(slug);
return {
title: `${post.title} | 我的博客`,
description: post.excerpt,
openGraph: {
title: post.title,
description: post.excerpt,
images: [post.coverImage],
type: 'article',
},
twitter: {
card: 'summary_large_image',
title: post.title,
},
};
}# 安装 Vercel CLI
pnpm add -g vercel
# 部署
vercel
# 生产环境部署
vercel --prodVercel 原生支持 Next.js 的所有特性(ISR、Middleware、Edge Runtime、Serverless Functions),零配置。
# Dockerfile
FROM node:20-alpine AS base
# 依赖安装阶段
FROM base AS deps
WORKDIR /app
COPY package.json pnpm-lock.yaml ./
RUN corepack enable pnpm && pnpm install --frozen-lockfile
# 构建阶段
FROM base AS builder
WORKDIR /app
COPY --from=deps /app/node_modules ./node_modules
COPY . .
RUN corepack enable pnpm && pnpm build
# 运行阶段
FROM base AS runner
WORKDIR /app
ENV NODE_ENV=production
RUN addgroup --system --gid 1001 nodejs
RUN adduser --system --uid 1001 nextjs
COPY --from=builder /app/public ./public
COPY --from=builder --chown=nextjs:nodejs /app/.next/standalone ./
COPY --from=builder --chown=nextjs:nodejs /app/.next/static ./.next/static
USER nextjs
EXPOSE 3000
CMD ["node", "server.js"]// next.config.ts — 启用 standalone 输出
export default {
output: 'standalone',
};// next.config.ts
export default {
output: 'export', // 导出纯静态 HTML
images: {
unoptimized: true, // 静态导出不支持图片优化
},
};局限: 静态导出不支持 SSR、ISR、Middleware、Route Handlers、动态路由的 revalidate 等。
| 方案 | SSR | ISR | Middleware | Server Actions | 成本 | 复杂度 |
|---|---|---|---|---|---|---|
| Vercel | 支持 | 支持 | 支持 | 支持 | 免费额度够用 | 最低 |
| Docker 自托管 | 支持 | 支持 | 支持 | 支持 | 服务器费用 | 中等 |
| 静态导出 + CDN | 不支持 | 不支持 | 不支持 | 不支持 | 极低 | 低 |
| AWS Lambda | 支持 | 有限 | 支持 | 支持 | 按请求计费 | 高 |
① 纯后台管理系统
内部使用的管理系统,不需要 SEO,不需要 SSR,用户量有限:
推荐方案:Vite + React SPA + antd
原因:
- SPA 更简单,不需要理解 Server/Client Components 边界
- 构建更快,部署更简单(扔到 CDN 即可)
- 表单交互密集,Next.js 的服务端渲染反而增加复杂度
② 高交互的 Web 应用
在线编辑器、协作白板、实时游戏等:
推荐方案:Vite + React + WebSocket
原因:
- 这类应用的状态管理极其复杂,SSR 带来的收益趋近于零
- 首屏渲染不是瓶颈,持续交互的性能才是
- 大量客户端状态,Server Components 无法参与
③ 纯静态展示站
只有几个页面的公司官网、活动页:
推荐方案:Astro / 纯 HTML
原因:
- Astro 生成的 JS 体积更小(默认零 JS)
- 构建速度更快
- Next.js 的 SSR/ISR 能力完全浪费
④ 已有成熟的 SPA 项目
现有项目运行良好,没有 SEO 需求:
推荐方案:维持现状
原因:
- 迁移成本高,需要重构路由、数据获取、组件拆分
- 如果只是想用 React Server Components,可以渐进引入(但心智负担大)
- "技术上更先进" 不是重构的理由,业务价值才是
框架层面:
- Vendor Lock-in 风险:深度使用 Vercel 特有功能(ISR、Edge Middleware)后,迁移成本高
- 构建速度:App Router 的构建比 Pages Router 慢,大型项目可能需要 2-5 分钟
- 缓存复杂度:多级缓存机制难以理解和调试,
cache行为在版本间有过破坏性变更 - Server Components 生态不成熟:很多 UI 库尚未适配 RSC,强制
'use client'后失去服务端优势
运维层面:
- Node.js 运行时依赖:SSR/ISR 需要服务器运行 Node.js,不像纯静态站可以完全依赖 CDN
- 内存泄漏风险:Server Components 中的不当操作可能导致服务端内存泄漏
- 冷启动延迟:Serverless 部署时,首次请求响应较慢(Vercel 通过 OOM 缓解,但仍有感知)
你的项目需要 SEO 吗?
├── 不需要
│ └── 是管理后台或内部工具吗?
│ ├── 是 → Vite + React SPA
│ └── 否 → 是高交互应用吗?
│ ├── 是 → Vite + React SPA + 状态管理
│ └── 否 → Next.js(CSR 模式也可以)
└── 需要
└── 内容更新频率?
├── 几乎不变 → Astro / Next.js SSG
├── 定期更新 → Next.js ISR
└── 实时更新 → Next.js SSR
- Next.js 解决的核心问题是 SSR + 全栈一体化,不是所有项目都需要这个能力
- App Router + Server Components 是 React 的未来方向,但学习曲线较陡
- Server/Client Components 的边界划分是写出高性能 Next.js 应用的关键
- 多级缓存机制强大但复杂,需要刻意理解才能用好
| 维度 | Next.js | Vite SPA | Astro |
|---|---|---|---|
| SSR/SSG | 原生支持 | 需要自己实现 | 原生支持 |
| SEO | 好 | 差(需预渲染) | 好 |
| 全栈能力 | Server Actions + API Routes | 需要独立后端 | 有限 |
| 学习成本 | 高(RSC + 缓存) | 低 | 中 |
| 构建速度 | 慢 | 快 | 中 |
| 交互密集应用 | 可以但非最优 | 最优 | 不适合 |
| 部署灵活度 | Vercel 最优 | 任意静态服务器 | 任意静态服务器 |
Next.js 是需要 SEO 和全栈能力的 React 项目的最佳选择,但不要为了"技术先进"而在不需要 SSR 的项目中使用它。