Next.js App Router 实战:让你的 React 项目快到飞起的 10 个核心技巧
如果你还在用 Pages Router,是时候升级了。Next.js 13 引入的 App Router 不仅仅是一个路由器的升级,它代表了一种全新的 React 开发范式。本文将从实战角度出发,分享 10 个让项目性能飙升的核心技巧。
1. 理解 Server Components vs Client Components
这是 App Router 最核心的概念,也是最容易踩坑的地方。
// 服务端组件 - 默认类型,不打包到客户端
async function ServerComponent() {
const data = await fetchData(); // 直接在服务器执行
return <div>{data.title}</div>;
}
// 客户端组件 - 需要交互时使用
'use client';
function ClientComponent() {
const [count, setCount] = useState(0);
return <button onClick={() => setCount(c => c + 1)}>{count}</button>;
}
原则:默认使用 Server Components,只在真正需要客户端交互时才加 'use client'。这能显著减少 JS bundle 大小。
2. 巧用 streaming 实现即时加载
传统 SSR 是等全部数据准备好才返回页面,现在你可以流式返回:
// app/page.tsx - 并行加载数据,但流式返回
import { Suspense } from 'react';
import { ProductList, ProductReviews } from './components';
export default function Page() {
return (
<main>
<h1>商品详情</h1>
<Suspense fallback={<ProductSkeleton />}>
<ProductList />
</Suspense>
<Suspense fallback={<ReviewsSkeleton />}>
<ProductReviews />
</Suspense>
</main>
);
}
关键点:两个组件可以并行加载,各自独立流式渲染,用户不用等待所有数据都准备好才能看到内容。
3. 深度利用 React Cache 减少重复请求
同一个数据在不同组件要用?用 React Cache 缓存起来:
import { cache } from 'react';
export const getUser = cache(async (userId: string) => {
const res = await fetch(\`/api/users/\${userId}\`);
return res.json();
});
// 在多个组件中使用,只发一次请求
function UserProfile({ userId }: { userId: string }) {
const user = getUser(userId); // 首次调用,发请求
// ...
}
function UserAvatar({ userId }: { userId: string }) {
const user = getUser(userId); // 使用缓存,不再发请求
// ...
}
4. Route Handler 的正确打开方式
App Router 的 API 路由改成了 Route Handler,放在 app/api/ 目录下:
// app/api/posts/route.ts
import { NextRequest, NextResponse } from 'next/server';
export async function GET(request: NextRequest) {
const { searchParams } = new URL(request.url);
const page = searchParams.get('page') || '1';
const posts = await fetchPosts(+page);
return NextResponse.json({ posts });
}
export async function POST(request: NextRequest) {
const body = await request.json();
const post = await createPost(body);
return NextResponse.json(post, { status: 201 });
}
实战 tip:Route Handler 支持 Web Standard Request/Response,用起来非常自然。
5. 充分利用 Next.js Image 组件
图片优化是性能大头,Next.js 内置的 Image 组件帮你搞定一切:
import Image from 'next/image';
function OptimizedImage() {
return (
<Image
src="/product.jpg"
alt="产品图"
width={800}
height={600}
// 自动生成 WebP/AVIF,根据浏览器选择最优格式
// 自动懒加载
// 防止布局偏移(CLS)
placeholder="blur"
blurDataURL="data:image/jpeg;base64,..." // 模糊占位图
sizes="(max-width: 768px) 100vw, (max-width: 1200px) 50vw, 33vw"
/>
);
}
实测:一张 500KB 的 PNG 用 Image 组件后可能只有 30KB。
6. 使用 Server Actions 替代 API 调用
还在写 API 路由?Server Actions 让前后端调用更简洁:
// app/actions.ts
'use server';
export async function createPost(formData: FormData) {
const title = formData.get('title');
const content = formData.get('content');
// 直接操作数据库,不需要 API 路由
const post = await db.post.create({
title: title as string,
content: content as string
});
return post;
}
// app/page.tsx - 客户端调用
'use client';
import { createPost } from './actions';
function PostForm() {
async function handleSubmit(formData: FormData) {
'use server'; // 服务端执行
await createPost(formData);
}
return (
<form action={handleSubmit}>
<input name="title" />
<textarea name="content" />
<button type="submit">发布</button>
</form>
);
}
7. 掌握 layout.tsx 的正确用法
Layout 是 App Router 最强大的特性之一:
// app/layout.tsx - 根布局
export default function RootLayout({ children }: { children: React.ReactNode }) {
return (
<html>
<body>
<nav>导航栏</nav>
{children}
<footer>页脚</footer>
</body>
</html>
);
}
// app/dashboard/layout.tsx - 仪表盘布局
export default function DashboardLayout({
children,
sidebar
}: {
children: React.ReactNode;
sidebar: React.ReactNode;
}) {
return (
<div className="dashboard">
{sidebar}
<main>{children}</main>
</div>
);
}
关键理解:Layout 不会在导航时重新渲染,只渲染变化的 children。这对保持状态、减少重排很重要。
8. 正确使用 middleware 做权限控制
Middleware 是请求到达页面之前的守门员:
// middleware.ts
import { NextResponse } from 'next/server';
import type { NextRequest } from 'next/server';
export function middleware(request: NextRequest) {
const token = request.cookies.get('token');
// 未登录且访问受保护路由
if (!token && !request.nextUrl.pathname.startsWith('/login')) {
return NextResponse.redirect(new URL('/login', request.url));
}
// 已登录访问登录页
if (token && request.nextUrl.pathname.startsWith('/login')) {
return NextResponse.redirect(new URL('/dashboard', request.url));
}
return NextResponse.next();
}
export const config = {
matcher: ['/((?!api|_next/static|_next/image|favicon.ico).*)'],
};
优势:在页面渲染之前就完成权限判断,比客户端跳转体验好一百倍。
9. 预渲染策略:generateStaticParams
动态路由也能静态生成:
// app/posts/[slug]/page.tsx
export async function generateStaticParams() {
const posts = await getAllPosts();
return posts.map((post) => ({
slug: post.slug,
}));
}
// 或者增量静态生成
export const dynamicParams = true; // 允许生成新页面
export async function generateStaticParams() {
// 只预渲染热门文章,其他按需生成
const popularPosts = await getPopularPosts();
return popularPosts.map((post) => ({ slug: post.slug }));
}
这样热门文章在构建时就生成好,访问时直接返回 HTML,速度飞快。
10. 监控和优化 Core Web Vitals
用 Next.js 的 Analytics 监控性能:
// app/components/Analytics.tsx
'use client';
import { Analytics } from '@vercel/analytics/react';
export function AnalyticsWrapper() {
return <Analytics />;
}
// app/layout.tsx
import { AnalyticsWrapper } from './components/Analytics';
export default function RootLayout({ children }: { children: React.ReactNode }) {
return (
<html>
<body>
{children}
<AnalyticsWrapper />
</body>
</html>
);
}
同时在 next.config.js 中配置实验性功能:
module.exports = {
experimental: {
optimizePackageImports: ['lucide-react', 'radix-ui'],
},
};
总结
App Router 带来的不只是一个新的路由系统,而是一种新的思维模式:
- 默认服务端:能服务端处理的就不要发到客户端
- 流式渲染:不要让用户等待所有资源
- 缓存为王:充分利用 Next.js 的缓存机制
- 零客户端 JS:能用 HTML/CSS 解决的就不要用 JS
掌握这些技巧,你的 React 项目性能会提升显著。在实际项目中,建议先从简单场景开始尝试,逐步迁移,积累经验后再大规模应用。
记住:性能优化是一个持续的过程,不要为了优化而优化,用户体验才是最终目标。
评论区
登录 后参与评论