Next.js 15 App Router实战:服务端组件与流式渲染性能优化

Next.js 15App 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(配合useFormStatususeFormState):

// 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/

(0)
小编小编
上一篇 20小时前
下一篇 20小时前

相关推荐