革命:async 组件

Next.js 13+ App Router 里,Server Component 可以直接是 async 函数

async function PostList() {
    const res = await fetch('https://api.example.com/posts');
    const posts = await res.json();

    return (
        <ul>
            {posts.map(p => <li key={p.id}>{p.title}</li>)}
        </ul>
    );
}

没 useEffect、没 useState、没 loading state——服务端跑完直接吐 HTML 给浏览器。

对比旧的 React 风格

// ❌ 老式(仍在 Client Component 用)
'use client';
import { useState, useEffect } from 'react';

function PostList() {
    const [posts, setPosts] = useState([]);
    const [loading, setLoading] = useState(true);
    const [error, setError] = useState(null);

    useEffect(() => {
        fetch('/api/posts')
            .then(r => r.json())
            .then(setPosts)
            .catch(setError)
            .finally(() => setLoading(false));
    }, []);

    if (loading) return <p>Loading...</p>;
    if (error) return <p>Error: {error.message}</p>;
    return <ul>{posts.map(p => <li>{p.title}</li>)}</ul>;
}
// ✓ 新式(Server Component)
async function PostList() {
    const posts = await getPosts();
    return <ul>{posts.map(p => <li>{p.title}</li>)}</ul>;
}

代码量减少 80%,且没有任何客户端 JS——HTML 直接生成完发给浏览器,SEO 完美

直接连数据库

Server Component 直接跑在服务端 → 可以直接连数据库

// src/lib/db.ts
import { Pool } from 'pg';
export const pool = new Pool({ connectionString: process.env.DATABASE_URL });

// src/app/users/page.tsx
import { pool } from '@/lib/db';

export default async function UsersPage() {
    const { rows } = await pool.query('SELECT * FROM users LIMIT 20');
    return (
        <ul>
            {rows.map(u => <li key={u.id}>{u.name}</li>)}
        </ul>
    );
}

没有 API endpoint,没有 fetch——服务端组件直接读 DB。

缓存(Next.js 15+ 起的新默认)

重要变化:Next.js 15 起,fetch() 默认不缓存——和原生 web fetch 行为一致。要缓存必须显式 opt-in。这与 Next.js 13/14 默认缓存的行为相反。

// 默认:每次请求都重新拉(等同 cache: 'no-store')
const data = await fetch('https://api.example.com/data');

// 显式强制缓存(构建期 + 运行期,相当于以前的默认)
fetch('https://api.example.com/data', { cache: 'force-cache' });

// 定时重新验证(每 60 秒)——也算缓存
fetch('https://api.example.com/data', { next: { revalidate: 60 } });

// 标签化(按需重新验证,配 revalidateTag)
fetch('https://api.example.com/data', { next: { tags: ['posts'], revalidate: 3600 } });

如果想给整个路由统一缓存策略,在 page / layout 顶部:

export const dynamic = 'force-static';   // 整页 SSG
// 或 'force-dynamic' 整页 SSR

标签按需 revalidate

// 在 Server Action 里触发
'use server';
import { revalidateTag } from 'next/cache';

export async function publishPost() {
    await db.publish(...);
    revalidateTag('posts');         // 所有打了 'posts' 标签的 fetch 重新拉
}

流式 + Suspense

慢数据用 Suspense 包:

import { Suspense } from 'react';

export default function Page() {
    return (
        <div>
            <h1>Posts</h1>
            <Suspense fallback={<p>Loading...</p>}>
                <PostList />
            </Suspense>
        </div>
    );
}

async function PostList() {
    const posts = await getPostsSlowly();      // 慢
    return <ul>{posts.map(...)}</ul>;
}

页面立刻显示 h1,PostList 加载完后流式补上——用户感知更快。

并行 fetch

// ❌ 顺序
async function Page() {
    const user = await getUser();        // 等
    const posts = await getPosts();      // 再等
    // 总耗时 = user + posts
}

// ✓ 并行
async function Page() {
    const [user, posts] = await Promise.all([
        getUser(),
        getPosts(),
    ]);
    // 总耗时 = max(user, posts)
}

动态参数

// src/app/blog/[slug]/page.tsx
async function getPost(slug: string) {
    const res = await fetch(`https://api.example.com/posts/${slug}`);
    return res.json();
}

// Next.js 15+:params 是 Promise,必须 await
export default async function BlogPost({ params }: { params: Promise<{ slug: string }> }) {
    const { slug } = await params;
    const post = await getPost(slug);
    return (
        <article>
            <h1>{post.title}</h1>
            <div dangerouslySetInnerHTML={{ __html: post.content }} />
        </article>
    );
}

错误处理

async function PostList() {
    try {
        const posts = await getPosts();
        return <ul>{posts.map(p => <li>{p.title}</li>)}</ul>;
    } catch (err) {
        return <p>Failed to load posts</p>;
    }
}

或用 error.tsx 边界(上一篇讲过)。

何时还需要 Client Component fetch

Server Component 不能用的场景:

  • 用户在浏览器交互后才知道要请求什么(搜索框 / 滚动加载 / 实时图)
  • 需要 useState / useEffect / 浏览器 API
  • 需要事件处理

这时用 Client Component + TanStack Query / SWR(react-advanced/16 篇细讲)。

  • Server Component 里不能用 useState / useEffect / onClick
  • Next.js 15+ fetch 默认缓存——想要 SSG / ISR 必须显式 cache: 'force-cache'next: { revalidate }
  • Next.js 15+ params / searchParamsPromise——必须 await,别再当对象直接用
  • 数据库连接不能在 Server Component 里反复建——用全局 pool(见上面例子)
  • 大数据 / 实时数据用 Client + TanStack Query

下一篇:图片 + 静态资源。