TL;DR — 核心要点
Next.js 15 App Router 是新项目的推荐方案。默认使用服务端组件,只在需要交互时添加 "use client"。Server Actions 替代 API 路由处理数据变更。结合 revalidatePath 的 ISR 兼顾静态与动态的优势。
关键要点
- App Router 默认使用 React 服务端组件,减少客户端 JS,提升性能
- 基于文件的路由系统:layout.tsx、loading.tsx、error.tsx 处理不同 UI 状态
- 数据获取直接在组件中使用 async/await,通过 fetch 选项控制缓存策略
- Server Actions 简化表单处理和数据变更,无需手写 API 路由
- next/image 和 next/font 自动优化媒体资源,提升 Core Web Vitals
- 中间件在 Edge Runtime 运行,适合身份验证和 A/B 测试
- standalone 输出模式支持 Docker 自托管部署
1. App Router vs Pages Router:如何选择
Next.js 13 引入了 App Router,这是一种基于 React Server Components 构建的新路由范式。它与原有的 Pages Router 并存,允许逐步迁移。理解两者的差异对于做出正确的架构决策至关重要。
| 特性 | App Router (推荐) | Pages Router (旧版) |
|---|---|---|
| 默认渲染 | React Server Components | 客户端组件 |
| 数据获取 | async/await + fetch cache | getStaticProps / getServerSideProps |
| 布局 | layout.tsx (嵌套) | _app.tsx (全局) |
| 加载状态 | loading.tsx (Suspense) | 手动实现 |
| 错误处理 | error.tsx (Error Boundary) | 手动实现 |
| Server Actions | 原生支持 | 需要 API 路由 |
| 流式传输 | 内置支持 | 不支持 |
| 推荐场景 | 所有新项目 | 维护现有项目 |
迁移路径:从 Pages Router 到 App Router
Next.js 支持在同一项目中同时使用两种路由器。迁移策略是逐页进行,从非关键路由开始。
// Directory structure showing coexistence
my-app/
app/ // App Router
layout.tsx // Root layout
page.tsx // Home page (migrated)
dashboard/
layout.tsx // Dashboard layout
page.tsx // Dashboard page (migrated)
pages/ // Pages Router (still active)
_app.tsx // Global wrapper
old-feature.tsx // Not yet migrated
api/
legacy-endpoint.ts // API route (still works)
// Migration checklist:
// 1. Move page.tsx from pages/ to app/
// 2. Replace getStaticProps with async component + fetch
// 3. Replace getServerSideProps with async Server Component
// 4. Add "use client" to components with hooks/events
// 5. Create layout.tsx for shared UI
// 6. Replace _document.tsx with root layout metadata2. Server Components vs Client Components
React Server Components(RSC)是 App Router 的核心。理解服务端组件和客户端组件的边界对于编写高性能的 Next.js 应用至关重要。
服务端组件(默认)
服务端组件在服务器上渲染,不向客户端发送任何 JavaScript。它们可以直接访问数据库、文件系统和私密 API。
// app/dashboard/page.tsx - Server Component (no "use client")
import { db } from "@/lib/db";
import { cache } from "react";
// cache() deduplicates requests across the render tree
const getUser = cache(async (id: string) => {
return db.user.findUnique({ where: { id } });
});
export default async function DashboardPage() {
// Direct database access - secure, no API needed
const user = await getUser("user_123");
const stats = await db.analytics.findMany({
where: { userId: user.id },
orderBy: { date: "desc" },
take: 30,
});
return (
<div>
<h1>Welcome, {user.name}</h1>
{/* Client component for interactivity */}
<StatsChart data={stats} />
</div>
);
}客户端组件("use client")
"use client" 指令将组件及其子树标记为客户端渲染。只在真正需要浏览器能力时使用它。
"use client";
// Must use "use client" when:
// - Using useState, useEffect, useRef, useContext
// - Adding event listeners (onClick, onChange, onSubmit)
// - Using browser APIs (window, localStorage, navigator)
// - Using third-party client-side libraries
import { useState, useEffect } from "react";
interface StatsChartProps {
data: { date: string; value: number }[];
}
export function StatsChart({ data }: StatsChartProps) {
const [activeIndex, setActiveIndex] = useState(0);
const [isVisible, setIsVisible] = useState(false);
useEffect(() => {
// Animate on mount - requires browser
setIsVisible(true);
}, []);
return (
<div
style={{ opacity: isVisible ? 1 : 0, transition: "opacity 0.3s" }}
>
{data.map((point, i) => (
<div
key={i}
onClick={() => setActiveIndex(i)}
style={{
background: i === activeIndex ? "#6366f1" : "#e2e8f0",
cursor: "pointer",
}}
>
{point.value}
</div>
))}
</div>
);
}Component Boundary 规则
"use client" 形成一个边界:该文件中的所有内容以及它导入的所有内容都变为客户端代码。将客户端组件放在组件树的叶子节点,保持服务端组件处理数据获取。
3. 文件系统路由:布局、模板与特殊文件
App Router 通过 app 目录中的文件和文件夹名称定义路由。特殊文件名约定控制每个路由段的行为。
app/
layout.tsx // Required: root layout, wraps all pages
page.tsx // Route: /
loading.tsx // Automatic Suspense boundary
error.tsx // Error boundary for this segment
not-found.tsx // 404 for this segment
template.tsx // Like layout but re-mounts on nav
global-error.tsx // Root error boundary
blog/
layout.tsx // Shared blog layout
page.tsx // Route: /blog
[slug]/
page.tsx // Route: /blog/:slug
opengraph-image.tsx // OG image generation
(marketing)/ // Route group - no URL segment
about/
page.tsx // Route: /about
pricing/
page.tsx // Route: /pricing
@modal/ // Parallel route slot
(.)photo/[id]/ // Intercepted route
page.tsxRoot Layout 必须包含的内容
// app/layout.tsx
import type { Metadata } from "next";
import { Inter } from "next/font/google";
const inter = Inter({ subsets: ["latin"] });
export const metadata: Metadata = {
title: {
template: "%s | My App",
default: "My App",
},
description: "My awesome application",
openGraph: {
type: "website",
siteName: "My App",
},
};
export default function RootLayout({
children,
}: {
children: React.ReactNode;
}) {
return (
<html lang="en">
<body className={inter.className}>
<Header />
<main>{children}</main>
<Footer />
</body>
</html>
);
}loading.tsx 和 Suspense 流式传输
loading.tsx 自动将页面包裹在 React Suspense 中。服务器会立即发送 HTML 外壳,然后流式传输内容块。
// app/dashboard/loading.tsx
// Shown instantly while dashboard/page.tsx loads
export default function DashboardLoading() {
return (
<div style={{ padding: "2rem" }}>
<div style={{
height: "2rem",
width: "40%",
background: "#e2e8f0",
borderRadius: "0.25rem",
animation: "pulse 2s infinite"
}} />
</div>
);
}
// Fine-grained streaming with Suspense
// app/dashboard/page.tsx
import { Suspense } from "react";
export default function DashboardPage() {
return (
<div>
{/* Renders immediately */}
<DashboardHeader />
{/* Streams when data resolves */}
<Suspense fallback={<RevenueChartSkeleton />}>
<RevenueChart />
</Suspense>
<Suspense fallback={<LatestInvoicesSkeleton />}>
<LatestInvoices />
</Suspense>
</div>
);
}4. 数据获取:fetch、ISR 与动态路由
App Router 中的数据获取在服务端组件中使用扩展的 fetch API 完成。Next.js 扩展了原生 fetch,添加了细粒度的缓存和重新验证控制。
fetch 缓存策略
// Next.js 15: fetch is NOT cached by default
// 1. Static data (CDN cached indefinitely)
const data = await fetch("https://api.example.com/static", {
cache: "force-cache",
});
// 2. Time-based revalidation (ISR)
const posts = await fetch("https://api.example.com/posts", {
next: { revalidate: 3600 }, // Revalidate every hour
});
// 3. Tag-based revalidation
const product = await fetch("https://api.example.com/products/1", {
next: { tags: ["product", "product-1"] },
});
// 4. Dynamic data (never cached)
const userProfile = await fetch("https://api.example.com/me", {
cache: "no-store",
headers: { Authorization: "Bearer " + getToken() },
});
// Trigger on-demand revalidation from a Server Action
import { revalidateTag, revalidatePath } from "next/cache";
async function updateProduct(id: string, data: Partial<Product>) {
"use server";
await db.product.update({ where: { id }, data });
revalidateTag("product-" + id); // Invalidate cached product data
revalidatePath("/products"); // Revalidate product list page
}generateStaticParams:SSG + 动态路由
// app/blog/[slug]/page.tsx
// Called at build time to pre-render pages
export async function generateStaticParams() {
const posts = await fetch("https://api.example.com/posts").then(
(res) => res.json()
);
return posts.map((post: { slug: string }) => ({
slug: post.slug,
}));
}
// Optionally control what happens for slugs not in generateStaticParams
// "blocking": generate on-demand and cache (default)
// false: 404 for unknown slugs
export const dynamicParams = true;
// Metadata generation
export async function generateMetadata({
params,
}: {
params: { slug: string };
}) {
const post = await getPost(params.slug);
return {
title: post.title,
description: post.excerpt,
openGraph: {
title: post.title,
images: [{ url: post.coverImage }],
},
};
}
export default async function BlogPost({
params,
}: {
params: { slug: string };
}) {
const post = await getPost(params.slug);
if (!post) notFound();
return (
<article>
<h1>{post.title}</h1>
<div dangerouslySetInnerHTML={{ __html: post.content }} />
</article>
);
}5. Server Actions:表单处理与数据变更
Server Actions 是标记了 "use server" 的异步函数,直接在服务器上运行。它们消除了为简单的 CRUD 操作手动创建 API 路由的需要。
基础 Server Action 与表单
// app/actions.ts
"use server";
import { revalidatePath } from "next/cache";
import { redirect } from "next/navigation";
import { z } from "zod";
const CreatePostSchema = z.object({
title: z.string().min(3).max(100),
content: z.string().min(10),
});
export type ActionState = {
errors?: { title?: string[]; content?: string[] };
message?: string;
};
export async function createPost(
prevState: ActionState,
formData: FormData
): Promise<ActionState> {
const validated = CreatePostSchema.safeParse({
title: formData.get("title"),
content: formData.get("content"),
});
if (!validated.success) {
return { errors: validated.error.flatten().fieldErrors };
}
try {
await db.post.create({ data: validated.data });
} catch (error) {
return { message: "Database error: Failed to create post." };
}
revalidatePath("/blog");
redirect("/blog");
}在客户端组件中使用 useFormState
"use client";
import { useFormState, useFormStatus } from "react-dom";
import { createPost, type ActionState } from "@/app/actions";
function SubmitButton() {
const { pending } = useFormStatus();
return (
<button type="submit" disabled={pending}>
{pending ? "Creating..." : "Create Post"}
</button>
);
}
const initialState: ActionState = {};
export function CreatePostForm() {
const [state, dispatch] = useFormState(createPost, initialState);
return (
<form action={dispatch}>
<div>
<label htmlFor="title">Title</label>
<input id="title" name="title" type="text" />
{state.errors?.title && (
<p style={{ color: "red" }}>{state.errors.title[0]}</p>
)}
</div>
<div>
<label htmlFor="content">Content</label>
<textarea id="content" name="content" />
{state.errors?.content && (
<p style={{ color: "red" }}>{state.errors.content[0]}</p>
)}
</div>
{state.message && (
<p style={{ color: "red" }}>{state.message}</p>
)}
<SubmitButton />
</form>
);
}最佳实践:Server Actions 与 API Routes
对于从 Next.js 应用内部触发的表单提交和数据变更,优先使用 Server Actions。对于需要被外部系统(移动应用、第三方服务、Webhooks)调用的端点,继续使用 Route Handlers(app/api/route.ts)。
6. 图片与字体优化
next/image 和 next/font 是 Next.js 中两个最强大的内置优化工具,对 Core Web Vitals 分数有直接影响。
next/image:自动格式转换与懒加载
import Image from "next/image";
// Local image: import for automatic size detection
import heroImage from "@/public/hero.jpg";
export function HeroSection() {
return (
<div style={{ position: "relative", height: "80vh" }}>
{/* LCP image: priority loads eagerly (no lazy) */}
<Image
src={heroImage}
alt="Hero illustration"
fill
priority // Removes loading="lazy" for LCP
sizes="100vw" // Responsive sizes hint
style={{ objectFit: "cover" }}
quality={85} // Default: 75 (AVIF/WebP auto-selected)
/>
</div>
);
}
// Remote image: requires explicit dimensions
export function ProductCard({ product }: { product: Product }) {
return (
<Image
src={product.imageUrl}
alt={product.name}
width={400}
height={300}
loading="lazy" // Default for non-priority images
placeholder="blur"
blurDataURL={product.blurHash}
/>
);
}配置远程域名
// next.config.ts
import type { NextConfig } from "next";
const nextConfig: NextConfig = {
images: {
remotePatterns: [
{
protocol: "https",
hostname: "cdn.example.com",
port: "",
pathname: "/images/**",
},
{
protocol: "https",
hostname: "**.cloudinary.com",
},
],
formats: ["image/avif", "image/webp"], // Preferred order
minimumCacheTTL: 60 * 60 * 24 * 30, // 30 days
},
};
export default nextConfig;next/font:零 CLS 字体加载
// app/layout.tsx
import { Inter, Fira_Code } from "next/font/google";
import localFont from "next/font/local";
const inter = Inter({
subsets: ["latin"],
display: "swap", // Font display strategy
variable: "--font-inter", // CSS variable for use in styles
});
const firaCode = Fira_Code({
subsets: ["latin"],
weight: ["400", "500"],
variable: "--font-fira-code",
});
// Self-hosted font
const brandFont = localFont({
src: [
{ path: "./fonts/Brand-Regular.woff2", weight: "400" },
{ path: "./fonts/Brand-Bold.woff2", weight: "700" },
],
variable: "--font-brand",
});
// Result: fonts are self-hosted by Next.js server,
// no external requests = no CLS, no privacy concerns
export default function RootLayout({ children }: { children: React.ReactNode }) {
return (
<html
lang="en"
className={inter.variable + " " + firaCode.variable + " " + brandFont.variable}
>
<body style={{ fontFamily: "var(--font-inter)" }}>{children}</body>
</html>
);
}7. 中间件与 Edge Runtime
Next.js 中间件在请求到达页面之前在 Edge Runtime(Vercel 的全球边缘网络,或本地 Node.js 的轻量化 V8 沙箱)运行,延迟极低。
身份验证与重定向中间件
// middleware.ts (at project root)
import { NextResponse } from "next/server";
import type { NextRequest } from "next/server";
import { verifyJWT } from "@/lib/auth";
export async function middleware(request: NextRequest) {
const { pathname } = request.nextUrl;
// --- Authentication check ---
if (pathname.startsWith("/dashboard")) {
const token = request.cookies.get("auth-token")?.value;
if (!token) {
return NextResponse.redirect(
new URL("/login?from=" + encodeURIComponent(pathname), request.url)
);
}
try {
const payload = await verifyJWT(token);
// Pass user info to the page via headers
const response = NextResponse.next();
response.headers.set("x-user-id", payload.sub);
response.headers.set("x-user-role", payload.role);
return response;
} catch {
return NextResponse.redirect(new URL("/login", request.url));
}
}
// --- A/B testing ---
if (pathname === "/pricing") {
const bucket = request.cookies.get("ab-bucket")?.value
?? (Math.random() < 0.5 ? "a" : "b");
const response = NextResponse.rewrite(
new URL("/pricing-" + bucket, request.url)
);
response.cookies.set("ab-bucket", bucket, { maxAge: 60 * 60 * 24 * 30 });
return response;
}
// --- Geolocation-based redirect ---
const country = request.geo?.country ?? "US";
if (country === "DE" && !pathname.startsWith("/de")) {
return NextResponse.redirect(new URL("/de" + pathname, request.url));
}
return NextResponse.next();
}
// Control which routes middleware applies to
export const config = {
matcher: [
// Match all routes except static files and API
"/((?!_next/static|_next/image|favicon.ico|api).*)",
],
};Edge Runtime 限制
Edge Runtime 不支持所有 Node.js API。无法使用 fs 模块、原生 Node.js 加密模块(使用 Web Crypto API 代替)或需要 Node.js 特定 API 的数据库驱动程序。对于这些操作,使用 Route Handlers(在 Node.js 运行时中运行)。
8. 部署:Vercel vs 自托管
Next.js 可以部署在任何支持 Node.js 的平台上。两种最常见的选择是 Vercel(官方平台)和使用 standalone 输出模式自托管。
Vercel 部署
# Deploying to Vercel is a single command
npx vercel
# Or push to main branch with Git integration
git push origin main # Auto-deploys via GitHub integration
# Environment variables
# Set in: vercel.com/project/settings/environment-variables
# Or via CLI:
vercel env add DATABASE_URL production独立模式 (standalone) + Docker
// next.config.ts
const nextConfig: NextConfig = {
output: "standalone", // Creates minimal server bundle
};
// Dockerfile (multi-stage build)
// Stage 1: Dependencies
// FROM node:20-alpine AS deps
// RUN apk add --no-cache libc6-compat
// WORKDIR /app
// COPY package.json package-lock.json ./
// RUN npm ci
//
// Stage 2: Builder
// FROM node:20-alpine AS builder
// WORKDIR /app
// COPY --from=deps /app/node_modules ./node_modules
// COPY . .
// ENV NEXT_TELEMETRY_DISABLED=1
// RUN npm run build
//
// Stage 3: Runner (minimal image ~150MB)
// FROM node:20-alpine AS runner
// WORKDIR /app
// ENV NODE_ENV=production
// COPY --from=builder /app/.next/standalone ./
// COPY --from=builder /app/.next/static ./.next/static
// COPY --from=builder /app/public ./public
// EXPOSE 3000
// CMD ["node", "server.js"]环境变量最佳实践
# .env.local (never commit - for local dev only)
DATABASE_URL=postgresql://user:pass@localhost/mydb
NEXTAUTH_SECRET=local-dev-secret
# .env (safe defaults, can be committed)
NEXT_PUBLIC_APP_URL=http://localhost:3000
NEXT_PUBLIC_ANALYTICS_ID=
// Accessing in code:
// Server-only (NOT prefixed with NEXT_PUBLIC_)
// Only accessible in Server Components, Route Handlers, Server Actions
const db = new PrismaClient({
datasources: { db: { url: process.env.DATABASE_URL } }
});
// Client-accessible (NEXT_PUBLIC_ prefix)
// Inlined at build time into the browser bundle
const analyticsId = process.env.NEXT_PUBLIC_ANALYTICS_ID;
// Type-safe env with T3 Env
// npm install @t3-oss/env-nextjs zod
import { createEnv } from "@t3-oss/env-nextjs";
import { z } from "zod";
export const env = createEnv({
server: {
DATABASE_URL: z.string().url(),
NEXTAUTH_SECRET: z.string().min(1),
},
client: {
NEXT_PUBLIC_APP_URL: z.string().url(),
},
runtimeEnv: process.env,
});9. 框架对比:Next.js vs Remix vs Nuxt vs SvelteKit
选择 Web 框架是一个重大决策。以下是 2026 年四个主要全栈框架的详细对比。
| 维度 | Next.js 15 | Remix 2 | Nuxt 3 | SvelteKit 2 |
|---|---|---|---|---|
| 底层框架 | React 19 | React 19 | Vue 3 | Svelte 5 |
| 默认渲染 | RSC (服务端) | SSR (服务端) | SSR (服务端) | SSR (服务端) |
| 数据获取 | fetch + cache / Server Components | loader() / action() | useFetch() / useAsyncData() | load() in +page.server.ts |
| 表单处理 | Server Actions | action() (native forms) | useFetch / Server Routes | form actions (+page.server.ts) |
| 路由类型 | 文件系统 (app/) | 文件系统 (routes/) | 文件系统 (pages/) | 文件系统 (+page.svelte) |
| 样式方案 | 任意 (Tailwind 官方支持) | 任意 | 任意 (UnoCSS 推荐) | Scoped CSS (内置) |
| TS 支持 | 优秀 (内置) | 优秀 (内置) | 优秀 (内置) | 优秀 (内置) |
| Bundle 大小 (框架) | ~85 KB (React runtime) | ~85 KB (React runtime) | ~100 KB (Vue runtime) | ~10-30 KB (compiled) |
| 学习曲线 | 中等 (RSC 概念) | 中等 (web standards) | 低-中 (Vue 生态) | 低 (Svelte 直观) |
| GitHub Stars | ~130K | ~30K | ~55K | ~19K |
| 最适合 | 全栈 React 应用、企业级 | 以表单为中心的应用、Web 标准优先 | Vue 团队、内容站点 | 性能优先、小型团队 |
| 生态系统 | 非常庞大 (React) | 大 (React) | 大 (Vue) | 中等 (成长中) |
何时选择 Remix
Remix 对 Web 标准(Fetch API、FormData、HTTP 语义)的强调使其代码更具可移植性,且不依赖框架抽象。如果你的团队重视表单的渐进增强(无 JavaScript 也能工作),Remix 是一个强有力的选择。
何时选择 SvelteKit
Svelte 通过编译器消除虚拟 DOM,生成非常小的 bundle。SvelteKit 非常适合需要最佳运行时性能的项目,以及不需要 React 生态系统的中小型团队。
10. 常见问题(FAQ)
Q1: App Router 和 Pages Router 有什么区别?
App Router 使用 React 服务端组件(默认)、嵌套布局、流式传输和 Server Actions。Pages Router 使用 getStaticProps/getServerSideProps 的传统模式。新项目推荐 App Router,旧项目可以逐步迁移。
Q2: 什么时候应该使用 "use client"?
当组件需要以下功能时使用 "use client":useState/useEffect 等 React 钩子、onClick/onChange 等事件监听器、浏览器 API(window、localStorage)、第三方客户端库。所有其他组件应保持为服务端组件以减少 JS 包大小。
Q3: Next.js 15 的缓存机制是如何工作的?
Next.js 15 改变了默认缓存行为:fetch 请求默认不再缓存(从 force-cache 改为 no-store)。使用 cache: "force-cache" 或 next.revalidate 显式控制缓存。Route handlers 也默认不缓存。这使缓存行为更可预测。
Q4: React 服务端组件(RSC)如何提升性能?
RSC 在服务器上渲染,不向客户端发送 JavaScript。这意味着:更小的 JS 包(组件代码不会被打包)、更快的 FCP(服务器直接发送 HTML)、安全的服务器端数据获取(可直接访问数据库和 API 密钥)、零客户端重新渲染(对于纯展示内容)。
Q5: Server Actions 是什么,如何使用它们?
Server Actions 是运行在服务器上的异步函数,用 "use server" 指令标记。它们可以直接在表单的 action 属性或事件处理器中调用。适合处理表单提交、数据库操作和文件上传,无需手动创建 API 路由。
Q6: ISR(增量静态再生)是什么?
ISR 允许你在构建后更新静态页面,无需重建整个站点。使用 next.revalidate 选项设置重新验证间隔(秒),或使用 revalidatePath/revalidateTag 按需触发重新验证。结合了静态页面的性能和动态内容的灵活性。
Q7: Next.js 中间件有什么用途?
Next.js 中间件在请求完成前运行,适用于:身份验证检查和重定向、A/B 测试(地理位置/cookies)、速率限制、请求头修改、国际化(根据语言重定向)、功能标志。中间件在 Edge Runtime 运行,延迟极低。
Q8: Next.js 应该部署在 Vercel 还是自托管?
Vercel 提供最佳的 Next.js 集成,支持 ISR、Edge Functions、Analytics 和零配置部署,适合大多数项目。自托管(standalone 输出模式 + Docker)适合需要完全控制基础设施、有合规要求或成本敏感的项目。两种方式都完全支持所有 Next.js 功能。
11. 生产环境性能优化清单
在将 Next.js 应用推向生产之前,请检查以下优化点以确保最佳性能。
服务端组件
- 最大化服务端组件的使用
- 将 "use client" 推向组件树叶子
- 避免在客户端组件中导入大型库
- 使用 React.lazy() 动态导入
图片与字体
- 所有图片使用 next/image
- LCP 图片添加 priority prop
- 提供正确的 sizes 属性
- 使用 next/font 避免布局偏移
缓存策略
- 静态内容使用 force-cache
- 动态内容设置适当的 revalidate
- 使用 unstable_cache 缓存数据库查询
- 实施 revalidateTag 按需失效
包大小
- 使用 @next/bundle-analyzer 分析
- 按需导入(避免 import * from)
- 懒加载非关键第三方脚本
- 使用 next/script 策略加载脚本
使用 Bundle Analyzer 分析包大小
# Install bundle analyzer
npm install @next/bundle-analyzer
// next.config.ts
import withBundleAnalyzer from "@next/bundle-analyzer";
const withAnalyzer = withBundleAnalyzer({
enabled: process.env.ANALYZE === "true",
});
export default withAnalyzer(nextConfig);
# Generate and open the bundle analysis report
ANALYZE=true npm run build
# Example: Dynamic import for heavy charting library
import dynamic from "next/dynamic";
const HeavyChart = dynamic(
() => import("@/components/HeavyChart"),
{
loading: () => <p>Loading chart...</p>,
ssr: false, // Skip SSR for browser-only lib
}
);
// Third-party script with strategy
import Script from "next/script";
export function Analytics() {
return (
<Script
src="https://analytics.example.com/script.js"
strategy="lazyOnload" // afterInteractive | lazyOnload | beforeInteractive
onLoad={() => console.log("Analytics loaded")}
/>
);
}总结
Next.js 15 的 App Router 代表了 React 全栈开发的成熟范式。React 服务端组件减少了客户端 JavaScript,Server Actions 简化了数据变更流程,而内置的图片、字体优化和中间件使得构建生产级应用更加简单。
关键建议:从 App Router 开始新项目,默认使用服务端组件,仅在需要交互性时使用 "use client"。用 Server Actions 替代简单的 API 路由,用 ISR(revalidate + revalidatePath)处理动态内容。部署时,Vercel 提供最少配置,而 standalone 模式 + Docker 给予完全的基础设施控制。
相关工具
使用 DevToolBox 的 JSON Formatter 调试 API 响应,Regex Tester 验证路由匹配模式,以及 JWT Decoder 检查 Next-Auth 生成的令牌。