跳到主要内容

Next.js 渲染与数据获取

这一块是 Next.js 最核心的基础之一。很多看起来像“API 记忆题”的东西,背后其实都在回答同一个问题:

  • 这段内容什么时候生成
  • 在服务端还是客户端生成
  • 数据什么时候请求
  • 请求结果缓存多久

先把几种渲染方式分开

1. CSR

客户端渲染。HTML 先到浏览器,再由 JS 拉数据并渲染。

适合:

  • 强交互页面
  • SEO 不敏感页面
  • 依赖浏览器状态较重的页面

2. SSR

服务端渲染。每次请求到来时,在服务端生成 HTML。

适合:

  • 首屏内容依赖请求上下文
  • SEO 重要
  • 页面内容需要实时性

3. SSG

静态生成。构建时就生成 HTML。

适合:

  • 内容更新频率低
  • 页面结构稳定
  • 营销页、文档页、博客页

4. ISR

增量静态再生成。页面先静态输出,再按设定时间重建。

适合:

  • 不需要每次请求都实时生成
  • 但又不能永远是构建时内容

Pages Router 的数据获取方式

getStaticProps

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

return {
props: { posts },
revalidate: 60,
};
}

getServerSideProps

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

return {
props: { posts },
};
}

这套方式很清楚,但把“页面”和“取数生命周期”分成了两个地方去写。

App Router 的数据获取方式

App Router 里,服务端取数通常直接写进组件。

async function getPosts() {
const res = await fetch('https://api.example.com/posts', {
next: { revalidate: 60 },
});

if (!res.ok) {
throw new Error('failed to fetch posts');
}

return res.json();
}

export default async function PostsPage() {
const posts = await getPosts();

return (
<ul>
{posts.map((post: { id: number; title: string }) => (
<li key={post.id}>{post.title}</li>
))}
</ul>
);
}

这套写法的核心变化是:页面本身就是服务端执行单元。

Server Component 和 Client Component 怎么配合

默认是 Server Component

export default async function Page() {
const data = await fetch('https://api.example.com/profile').then((res) =>
res.json()
);

return <div>{data.name}</div>;
}

需要交互时再下放到 Client Component

'use client';

import { useState } from 'react';

export default function Counter() {
const [count, setCount] = useState(0);
return <button onClick={() => setCount(count + 1)}>{count}</button>;
}

一个很实用的经验是:

  • 先尽量让页面留在服务端
  • 只把真正需要交互、浏览器 API、状态管理的部分切到客户端

fetch 在 Next 里不只是浏览器那个 fetch

在 App Router 里,fetch 会接入 Next 的缓存和重验证模型。也就是说,写法看起来一样,但语义比普通 React 项目里的 fetch 更重。

await fetch('https://api.example.com/products', {
cache: 'force-cache',
});

await fetch('https://api.example.com/products', {
cache: 'no-store',
});

await fetch('https://api.example.com/products', {
next: { revalidate: 300 },
});

流式渲染和 Suspense

App Router 对流式渲染支持更自然。

import { Suspense } from 'react';

export default function DashboardPage() {
return (
<div>
<h1>Dashboard</h1>
<Suspense fallback={<div>Loading chart...</div>}>
<ChartSection />
</Suspense>
</div>
);
}

这意味着页面不必等所有慢数据都准备好之后再一次性返回。

客户端数据层什么时候还需要

即使在 App Router 里,React Query、SWR 这类客户端数据层仍然很有用。典型场景有:

  • 频繁轮询
  • 用户操作后的局部刷新
  • 客户端缓存复用
  • 多组件共享同一份客户端请求状态
'use client';

import { useQuery } from '@tanstack/react-query';

export default function Notifications() {
const { data, isLoading } = useQuery({
queryKey: ['notifications'],
queryFn: () => fetch('/api/notifications').then((res) => res.json()),
});

if (isLoading) return <div>Loading...</div>;
return <div>{data.length}</div>;
}

一个简单判断方式

优先服务端取数

适合:

  • 首屏需要内容
  • SEO 重要
  • 页面结构依赖后端数据
  • 不希望把大量请求逻辑下发到浏览器

优先客户端取数

适合:

  • 页面已经渲染出来后再做局部刷新
  • 交互频繁
  • 实时性强
  • 状态共享主要发生在客户端

常见误区

1. 以为 App Router 就完全不需要客户端取数

并不是。只是“服务端优先”变成了新的默认思路。

2. 以为 SSR、SSG、ISR 是三选一

现实项目里常常是混合的。不同页面、不同数据块可以走不同策略。

3. 低估 fetch 缓存语义

在 Next 里,fetch 已经不是单纯网络请求函数,它也是缓存配置入口。

推荐继续往下看

  1. 缓存与重验证
  2. Server Actions

参考资料