Next.js 15的App Router架构基于React Server Components(RSC),将组件渲染拆分为服务端和客户端两层,从架构层面减少客户端JavaScript体积。配合流式渲染(Streaming)和Suspense边界,首屏内容可以分块输出,显著改善FCP(First Contentful Paint)和LCP(Largest Contentful Paint)指标。本文通过实际配置案例讲解App Router的核心用法和性能优化策略。
App Router架构与服务端组件基础
App Router使用app/目录组织路由,每个目录代表一个路由段,page.tsx定义路由UI。默认所有组件都是Server Component,需要客户端交互的组件通过"use client"指令声明。
目录结构示例:
app/
├── layout.tsx # 根布局(Server Component)
├── page.tsx # 首页
├── blog/
│ ├── layout.tsx # 博客布局
│ ├── page.tsx # 博客列表
│ └── [slug]/
│ └── page.tsx # 博客详情
├── dashboard/
│ ├── layout.tsx # 仪表盘布局(需要认证)
│ └── page.tsx # 仪表盘首页
└── api/
└── users/
└── route.ts # API路由
Server Component示例(默认,无需声明):
// app/blog/[slug]/page.tsx
import { notFound } from 'next/navigation'
import { db } from '@/lib/db'
export default async function BlogPost({ params }: { params: { slug: string } }) {
const post = await db.post.findUnique({
where: { slug: params.slug }
})
if (!post) notFound()
return (
<article>
<h1>{post.title}</h1>
<time dateTime={post.createdAt.toISOString()}>
{post.createdAt.toLocaleDateString('zh-CN')}
</time>
<div dangerouslySetInnerHTML={{ __html: post.content }} />
</article>
)
}
Client Component示例:
// app/components/LikeButton.tsx
'use client'
import { useState } from 'react'
export function LikeButton({ initialCount }: { initialCount: number }) {
const [count, setCount] = useState(initialCount)
const [loading, setLoading] = useState(false)
async function handleLike() {
setLoading(true)
const res = await fetch('/api/like', { method: 'POST' })
const data = await res.json()
setCount(data.count)
setLoading(false)
}
return (
<button onClick={handleLike} disabled={loading}>
{loading ? '...' : `点赞 ${count}`}
</button>
)
}
Server Component中直接使用Client Component:
// app/blog/[slug]/page.tsx
import { LikeButton } from '@/app/components/LikeButton'
export default async function BlogPost({ params }) {
const post = await getPost(params.slug)
return (
<article>
<h1>{post.title}</h1>
<LikeButton initialCount={post.likeCount} />
</article>
)
}
Server Component的数据通过props传递给Client Component,这个过程称为序列化。函数、Date对象等无法序列化的值不能直接传递。
数据获取与缓存策略:fetch缓存与ISR配置
Next.js 15扩展了原生fetch函数,增加缓存控制和重新验证选项。Server Component中直接调用fetch即可实现服务端数据获取。
fetch缓存配置:
// 默认缓存,后台定期重新验证
const res = await fetch('https://api.example.com/articles', {
next: { revalidate: 3600 } // 每小时重新验证
})
// 完全静态缓存(构建时获取)
const res = await fetch('https://api.example.com/config', {
next: { revalidate: false }
})
// 不缓存(每次请求都获取)
const res = await fetch('https://api.example.com/realtime', {
cache: 'no-store'
})
// 按标签批量重新验证
const res = await fetch('https://api.example.com/articles', {
next: { tags: ['articles'] }
})
// 在Server Action中触发重新验证
import { revalidateTag } from 'next/cache'
await revalidateTag('articles')
ISR(Incremental Static Regeneration)通过revalidate参数实现页面级缓存:
// app/blog/page.tsx
export const revalidate = 600 // 10分钟重新生成
export default async function BlogList() {
const articles = await fetch('https://api.example.com/articles', {
next: { tags: ['articles'] }
}).then(res => res.json())
return (
<ul>
{articles.map(a => (
<li key={a.id}><a href={`/blog/${a.slug}`}>{a.title}</a></li>
))}
</ul>
)
}
动态路由段的generateStaticParams预生成静态页面:
// app/blog/[slug]/page.tsx
export async function generateStaticParams() {
const posts = await db.post.findMany({ select: { slug: true } })
return posts.map(p => ({ slug: p.slug }))
}
// 配合dynamicParams控制未预生成页面的行为
export const dynamicParams = true // true=按需生成, false=返回404
流式渲染与Suspense边界配置
流式渲染允许服务端在数据准备完成前先发送页面骨架,数据就绪后通过React的流式HTML注入到对应位置。这能显著改善首屏渲染速度,用户无需等待所有数据加载完毕。
Suspense边界配置:
// app/dashboard/page.tsx
import { Suspense } from 'react'
import { Stats, RecentOrders, UserList } from '@/app/components/dashboard'
export default function Dashboard() {
return (
<div>
<h1>仪表盘</h1>
{/* 统计数据快速加载,优先展示 */}
<Suspense fallback={<div>加载统计数据...</div>}>
<Stats />
</Suspense>
{/* 订单列表较慢,延迟加载 */}
<Suspense fallback={<OrderSkeleton />}>
<RecentOrders />
</Suspense>
{/* 用户列表最慢,最后加载 */}
<Suspense fallback={<UserSkeleton />}>
<UserList />
</Suspense>
</div>
)
}
// 骨架组件
function OrderSkeleton() {
return (
<div className="animate-pulse space-y-3">
{[1, 2, 3].map(i => (
<div key={i} className="h-12 bg-gray-200 rounded" />
))}
</div>
)
}
每个Suspense边界独立流式输出。服务端先发送<div>加载统计数据...</div>和骨架组件,当Stats组件的数据就绪后,React将fallback替换为实际内容。不同Suspense边界之间互不阻塞。
Loading UI简化配置(loading.tsx):
// app/dashboard/loading.tsx
export default function Loading() {
return (
<div className="flex items-center justify-center min-h-[400px]">
<div className="animate-spin rounded-full h-12 w-12 border-b-2 border-blue-600" />
</div>
)
}
loading.tsx是Next.js的约定文件,等同于在整个页面外层包裹Suspense边界。当page.tsx的数据获取正在进行时,自动展示loading.tsx的内容。
Server Actions与表单处理
Server Actions允许在Server Component中定义服务端执行函数,无需手动创建API路由。表单提交直接调用Server Action完成数据处理。
// app/contact/page.tsx
import { revalidatePath } from 'next/cache'
import { redirect } from 'next/navigation'
async function submitContact(formData: FormData) {
'use server'
const name = formData.get('name') as string
const email = formData.get('email') as string
const message = formData.get('message') as string
if (!name || !email || !message) {
throw new Error('请填写所有字段')
}
await db.contact.create({
data: { name, email, message, createdAt: new Date() }
})
revalidatePath('/admin/contacts')
redirect('/contact/success')
}
export default function ContactPage() {
return (
<form action={submitContact}>
<input type="text" name="name" placeholder="姓名" required />
<input type="email" name="email" placeholder="邮箱" required />
<textarea name="message" placeholder="留言内容" required />
<button type="submit">提交</button>
</form>
)
}
带进度的Server Action(配合useFormStatus和useFormState):
// app/components/ContactForm.tsx
'use client'
import { useFormStatus, useFormState } from 'react-dom'
import { submitContact } from '@/app/actions/contact'
function SubmitButton() {
const { pending } = useFormStatus()
return (
<button type="submit" disabled={pending}>
{pending ? '提交中...' : '提交'}
</button>
)
}
export function ContactForm() {
const [state, formAction] = useFormState(submitContact, { error: null })
return (
<form action={formAction}>
<input type="text" name="name" required />
<SubmitButton />
{state.error && <p className="text-red-500">{state.error}</p>}
</form>
)
}
性能优化:代码分割与懒加载策略
Next.js 15自动对Client Component进行代码分割。对于大型第三方库,使用next/dynamic实现按需加载。
import dynamic from 'next/dynamic'
// 懒加载图表库,仅在客户端渲染
const Chart = dynamic(() => import('recharts').then(mod => mod.LineChart), {
ssr: false,
loading: () => <div className="h-64 bg-gray-100 animate-pulse" />
})
export default function Analytics() {
return (
<div>
<h1>数据分析</h1>
<Suspense fallback={<div>加载图表...</div>}>
<Chart data={data} />
</Suspense>
</div>
)
}
图片优化使用next/image自动实现懒加载、尺寸适配和格式转换:
import Image from 'next/image'
// 自动生成WebP/AVIF格式,按需加载
<Image
src="/hero.jpg"
alt="首页横幅"
width={1200}
height={600}
priority // 首屏图片设为priority,避免懒加载延迟LCP
sizes="(max-width: 768px) 100vw, 50vw"
placeholder="blur"
blurDataURL="data:image/jpeg;base64,..." // 模糊占位符
/>
字体优化使用next/font自动托管字体文件,消除外部请求:
import { Inter, Noto_Sans_SC } from 'next/font/google'
const inter = Inter({ subsets: ['latin'], variable: '--font-inter' })
const notoSC = Noto_Sans_SC({ subsets: ['latin'], weight: ['400', '700'], variable: '--font-noto-sc' })
export default function RootLayout({ children }) {
return (
<html lang="zh-CN" className={`${inter.variable} ${notoSC.variable}`}>
<body>{children}</body>
</html>
)
}
构建分析工具定位性能瓶颈:
# 构建分析
ANALYZE=true npm run build
# 或使用 @next/bundle-analyzer
npm install @next/bundle-analyzer
# 在next.config.js中配置后执行
npm run build
分析报告展示每个路由的JavaScript体积,识别过大的依赖包。App Router模式下,Server Component的代码不会打包到客户端,只有Client Component和"use client"标记的模块会进入客户端bundle。合理拆分Server/Client Component边界是控制客户端体积的核心策略。
原创文章,作者:小编,如若转载,请注明出处:https://www.yunthe.com/nextjs15approuter-shi-zhan-fu-wu-duan-zu-jian-yu-liu-shi/