1. 开篇:为什么你的Next.js项目需要选对渲染模式?
大家好,我是老张,一个在前后端领域摸爬滚打了十多年的老码农。这些年,我亲眼看着前端从刀耕火种的jQuery时代,一路狂奔到如今React、Vue、Next.js这些框架百花齐放。说实话,技术栈的快速迭代让人兴奋,但也带来了不少甜蜜的烦恼——选择太多了,反而不知道怎么选。
就拿Next.js来说,它确实是个好东西,把React应用的开发体验提升了一大截。但很多刚上手的朋友,甚至一些有经验的开发者,常常被它提供的几种渲染模式搞得晕头转向:CSR、SSR、SSG,还有ISR、RSC……这些缩写到底是什么意思?我的项目到底该用哪个?选错了会有什么后果?
我见过太多这样的场景:一个内容基本不变的官网,开发者却用了纯客户端渲染(CSR),结果老板抱怨“为什么我们的网站在谷歌上搜不到?”;又或者,一个需要实时展示股票价格的金融面板,却用了静态生成(SSG),导致用户看到的数据永远是昨天的。这些“坑”,我都亲自踩过,也帮团队填过。
所以,今天我们不聊深奥的源码,也不扯那些高大上的概念。我就想用最直白的话,结合我这些年做电商、博客、后台系统这些真实项目的经验,跟你好好掰扯一下Next.js里这几种核心的渲染模式。我们的目标很简单:看完这篇文章,你能像老手一样,根据自己项目的“脾气”,精准地选出最合适的那把“渲染钥匙”,让项目既跑得快,又吃得少(资源),还能被搜索引擎喜欢。
2. 三大核心渲染模式:CSR、SSR、SSG的本质区别
要做出正确选择,我们得先摸清这三位“主角”的底细。你可以把它们想象成三种不同的“餐厅后厨”工作模式。
2.1 CSR:客户端渲染——让浏览器当大厨
CSR(Client-Side Rendering),客户端渲染。这是传统React、Vue单页面应用(SPA)的默认方式。
它是怎么工作的? 想象一下,你去一家餐厅(访问网站),服务员(服务器)直接给了你一本空白的菜谱(一个几乎空的HTML外壳)和一大堆生鲜食材(JavaScript代码)。然后告诉你:“客官,菜谱和食材都在这儿了,您想吃什么,得自己动手炒。” 于是,你(浏览器)得先花时间看懂菜谱(解析JS),再去冰箱拿食材(发起API请求获取数据),最后开火炒菜(用JS生成DOM并渲染)。菜没炒好之前,你只能对着空盘子干等。
代码上长啥样?
在Next.js里,如果你在一个页面组件里直接用useEffect和fetch去拿数据,那这个页面就是CSR。
// pages/products.js - 一个典型的CSR页面
import { useState, useEffect } from 'react';
export default function ProductsPage() {
const [products, setProducts] = useState([]);
const [loading, setLoading] = useState(true);
useEffect(() => {
// 页面在浏览器渲染后,才发起请求获取数据
fetch('/api/products')
.then(res => res.json())
.then(data => {
setProducts(data);
setLoading(false);
});
}, []);
if (loading) return <div>加载中...(此时首屏是空的)</div>;
return (
<div>
<h1>产品列表</h1>
<ul>
{products.map(p => <li key={p.id}>{p.name}</li>)}
</ul>
</div>
);
}
优点与坑点:
- 优点:交互体验极其流畅。一旦首次“大厨上岗”完成(JS加载并执行),后续的页面切换就像在餐厅里换个座位看新菜谱,速度快,无刷新。前后端可以彻底分离,开发职责清晰。
- 坑点(也是我踩过的):
- 首屏白屏时间长:用户网络慢点、手机旧点,等待时间就很明显。我做过一个后台管理系统,初期用纯CSR,在客户的老旧电脑上打开,足足转了5秒圈圈,被吐槽惨了。
- SEO不友好:虽然谷歌爬虫现在能执行JS了,但等待和执行都有额外开销,而且其他搜索引擎支持程度不一。如果你的网站流量严重依赖搜索,CSR要慎用。
- 内容闪现:如果没处理好加载状态,页面可能先显示框架,再突然填充内容,体验割裂。
适合谁用?
- 后台管理系统/Dashboard:不需要SEO,用户登录后体验优先。
- 强交互的Web应用:比如在线设计工具、复杂的表单流程,交互比首屏速度更重要。
- Hybrid App内的Webview:环境可控,且通常已预加载资源。
2.2 SSR:服务端渲染——后厨帮你把菜炒好端上来
SSR(Server-Side Rendering),服务端渲染。Next.js的看家本领之一。
它是怎么工作的? 还是那家餐厅,但这次你一点菜,服务员就跑到后厨,让厨师(服务器)根据你的要求,用最新鲜的食材(实时调用API)把菜炒好,然后连盘子带菜(完整的HTML)一起端到你面前。你拿到手就是立即可吃的。当然,如果这道菜需要你后续自己加辣酱(交互),服务员会同时给你一罐辣酱(JS文件),等你需要时自己加。
Next.js里怎么实现?
在Pages Router中,使用 getServerSideProps 函数。
// pages/products.js - 一个SSR页面
export default function ProductsPage({ products }) {
// 产品数据在组件渲染前就已经作为props注入进来了
return (
<div>
<h1>产品列表</h1>
<ul>
{products.map(p => <li key={p.id}>{p.name}</li>)}
</ul>
</div>
);
}
// 这个函数在每次页面请求时,在服务端运行
export async function getServerSideProps() {
// 在服务端调用API,数据不会暴露给客户端
const res = await fetch('https://api.yoursite.com/products');
const products = await res.json();
// 返回的数据会作为 props 传递给页面组件
return {
props: {
products,
},
};
}
优点与挑战:
- 优点:
- 首屏速度杀手:用户第一时间就能看到完整内容,特别是对于弱网环境(地铁、电梯)的用户,体验提升巨大。我做电商项目时,把商品详情页改成SSR,首屏加载时间直接减少了40%。
- SEO满分:搜索引擎爬虫拿到的是渲染好的“熟HTML”,收录毫无压力。
- 社交分享友好:分享到微信、微博时,能正确抓取到页面标题、描述和图片。
- 挑战:
- 服务器压力大:每次请求都要干活,CPU、内存消耗高。流量一大,服务器成本就上去了。需要做好缓存和负载均衡。
- TTFB可能变慢:Time To First Byte,从点击到收到第一个字节的时间。因为服务器要现做菜,这个时间可能比直接端上一碗白米饭(CSR的空HTML)要长。
- 开发复杂度:你得考虑服务端环境(Node.js),注意代码的同构性(不能在服务端用
window对象),算是“全栈”初体验。
适合谁用?
- 电商网站的商品详情页、列表页:强SEO需求,且内容个性化(不同用户看到的价格、库存可能不同)。
- 内容型网站(新闻、博客)的详情页:特别是内容更新频繁,需要被快速收录的。
- 用户登录后的个性化主页:如社交媒体的信息流,每次请求都需要获取用户最新的动态。
2.3 SSG:静态站点生成——预制菜,微波炉热一下就能吃
SSG(Static Site Generation),静态站点生成。这是把“提前准备”做到极致的一种方式。
它是怎么工作的? 这家餐厅更厉害了,在每天开业前(构建时),就把所有可能的菜式都提前炒好几百份,分装好(生成静态HTML文件),放在保温柜(CDN)里。你一进门点菜,服务员直接从柜子里拿出对应的那份,用微波炉(几乎没有)热一下(直接传输)就端给你,速度极快。
Next.js里怎么实现?
在Pages Router中,使用 getStaticProps 函数。在App Router中,默认就是SSG(除非你用了动态函数)。
// pages/posts/[id].js - 一个SSG的动态路由页面
export default function BlogPost({ post }) {
return (
<article>
<h1>{post.title}</h1>
<div dangerouslySetInnerHTML={{ __html: post.content }} />
</article>
);
}
// 在构建时运行,获取所有可能的文章数据
export async function getStaticPaths() {
const res = await fetch('https://api.yoursite.com/posts');
const posts = await res.json();
const paths = posts.map((post) => ({
params: { id: post.id.toString() },
}));
return { paths, fallback: false }; // fallback 很重要,后面会讲
}
// 针对每个路径,在构建时运行一次,生成静态页面
export async function getStaticProps({ params }) {
const res = await fetch(`https://api.yoursite.com/posts/${params.id}`);
const post = await res.json();
return {
props: {
post,
},
};
}
优点与局限:
- 优点(性能怪兽):
- 首屏加载极快:直接从CDN返回HTML,几乎没有计算延迟。我司的技术博客改用SSG后,Lighthouse性能评分接近满分。
- 服务器成本极低:CDN扛住了几乎所有流量,源服务器几乎在睡觉。安全性也高,因为服务器不执行动态代码。
- SEO极致友好:静态文件是搜索引擎爬虫的最爱。
- 局限:
- 数据不是实时的:构建时是什么数据,页面显示就是什么。文章更新了?得重新构建部署。
- 构建时间可能很长:如果你的网站有十万个商品详情页,每次构建都生成十万个HTML文件,那构建过程会非常漫长。
适合谁用?
- 技术文档、博客、企业官网:内容相对固定,更新不频繁。
- 营销落地页、活动页:追求极致的打开速度。
- 产品展示页:商品信息一天甚至一周才更新一次。
3. 实战选择指南:根据你的项目场景做决策
理论说了一堆,到底怎么选?我画了张简单的决策图,你可以先有个直观感受:
项目启动
|
v
[内容是否几乎不变?]
/ \
是 否
/ \
v v
使用 SSG [是否需要最佳SEO和首屏速度?]
(博客、文档) / \
是 否
/ \
v v
[数据是否高度个性化/实时?] 使用 CSR
/ \ (后台、工具)
是 否
/ \
v v
使用 SSR 使用 SSG + ISR
(电商详情、社交动态) (新闻、产品目录)
当然,图是死的,项目是活的。我们来深入几个典型场景聊聊。
3.1 场景一:电商网站——混合策略的艺术
电商是最复杂的场景之一,一个网站里不同页面需求截然不同。
- 首页、分类页:这部分内容相对稳定,但可能需要根据运营活动频繁调整。我推荐 SSG with ISR(增量静态再生)。比如设置
revalidate: 3600,让它每小时在后台重新生成一次,既能享受静态页面的速度,又能保证内容不会太过时。// 在 getStaticProps 返回的对象中增加 revalidate 字段 export async function getStaticProps() { const res = await fetch('https://api.yoursite.com/homepage-data'); const data = await res.json(); return { props: { data }, revalidate: 3600, // 每3600秒(1小时)重新验证并可能重新生成 }; } - 商品详情页(PDP):这是流量和转化的核心。需要强SEO,且数据(价格、库存、评论)高度动态。SSR是更稳妥的选择。确保用户每次打开都是最新信息,尤其是做秒杀活动时。
- 购物车、用户中心:这部分是登录后场景,SEO不重要,但交互复杂。CSR是更好的选择,可以提供无缝的SPA体验。Next.js允许你在同一个应用里混合使用,购物车页面完全可以用客户端组件和CSR逻辑。
我踩过的坑:曾经试图用SSG做所有商品页,结果库存状态不准,被客户投诉。后来改成SSR+Redis缓存API响应,平衡了性能和实时性。
3.2 场景二:内容博客/新闻站——在速度与新鲜度间平衡
- 文章列表页:新文章发布不频繁。可以用 SSG,构建时生成。如果担心新文章发布后列表页不更新,可以用 ISR,设置一个较长的
revalidate时间(比如300秒)。 - 文章详情页:这是核心。新文章发布后需要立刻被收录。SSG + On-demand Revalidation(按需重新验证) 是Next.js提供的利器。你可以在CMS(内容管理系统)后台发布文章时,调用Next.js提供的一个特定API接口,触发某篇文章详情页的重新构建,而不用重建整个站点。
// 假设你的文章详情页路径是 /posts/[slug] // 在CMS后台,文章发布后调用: // POST https://yournextjsapp.com/api/revalidate?secret=<token>&path=/posts/my-new-article - 评论区域:文章内容是静态的,但评论是动态的。这里可以用 SSG + 客户端Hydration。页面主体(文章)静态生成,评论区域用一个客户端组件来渲染,页面加载后再用CSR的方式获取并显示最新评论。这就是所谓的“部分Hydration”。
3.3 场景三:后台管理系统/仪表盘——体验至上
这个最简单。几乎全部使用CSR。因为:
- 无需SEO。
- 用户都是登录后使用,首屏加载慢一点可以接受(配合加载动画)。
- 交互极其复杂,表格、图表、表单联动,CSR的流畅度优势巨大。
- 数据高度个性化、实时性要求高(实时监控数据)。
在Next.js里,你只需要创建普通的页面组件,在里面用useEffect或SWR、TanStack Query这些客户端数据获取库去拿数据就行了。甚至可以完全不用getServerSideProps或getStaticProps。
4. 进阶玩法与常见陷阱
4.1 Hydration(水合):让静态页面“活”过来的关键
这是理解SSR和SSG的核心。我打个比方:SSR/SSG给你的页面就像一尊制作精良的蜡像(HTML),看起来栩栩如生,但它是死的,不能动。Hydration的过程,就是给这尊蜡像注入灵魂(JS交互逻辑),让它变成活人。
过程详解:
- 服务器返回已经包含数据的HTML。
- 浏览器立即渲染,用户看到内容(蜡像)。
- 浏览器在后台加载JS包。
- React运行,调用
hydrateRoot(),将自己的虚拟DOM与浏览器中已有的真实DOM进行“对比”。 - 如果结构一致,React就将事件处理器“绑定”到现有的DOM节点上(注入灵魂)。
- 页面变得可交互。
我遇到的Hydration错误:最常见的是“服务器和客户端渲染内容不一致”。比如,你在组件里直接用了window.innerWidth。在服务器上,window是undefined,渲染出的HTML可能是一个默认值。在客户端,window.innerWidth是一个具体数字。React在Hydration时发现对不上,就会报错,并丢弃服务器渲染的HTML,完全改用客户端渲染,这就浪费了SSR的努力。
解决方案:将依赖于浏览器API的代码放到useEffect里执行,或者用typeof window !== 'undefined'进行判断。
function MyComponent() {
// ❌ 错误:服务端渲染时 window 未定义
// const [width, setWidth] = useState(window.innerWidth);
// ✅ 正确:在客户端Effect中初始化
const [width, setWidth] = useState(0); // 先给个默认值
useEffect(() => {
setWidth(window.innerWidth);
const handleResize = () => setWidth(window.innerWidth);
window.addEventListener('resize', handleResize);
return () => window.removeEventListener('resize', handleResize);
}, []);
// ... 使用 width
}
4.2 混合渲染与App Router的新范式
Next.js 13推出的App Router和React Server Components(RSC)带来了一种更细粒度的混合渲染思维。它不再单纯以页面为单位划分SSR/SSG/CSR,而是以组件为单位。
- 服务端组件(默认):在服务器上渲染,不包含交互逻辑,代码不会被打包到客户端bundle。适合渲染静态内容、获取敏感数据(直接访问数据库)。
- 客户端组件(
‘use client’):在客户端渲染和交互,代码会打到客户端bundle。
你可以在一个页面里自由组合。比如一个博客页面:
// app/blog/[slug]/page.js
// 这是一个服务端组件(默认)
import { getPost } from '@/lib/db'; // 直接访问数据库
import Comments from './Comments'; // 这是一个客户端组件
export default async function Page({ params }) {
const post = await getPost(params.slug); // 在服务端获取数据
return (
<article>
<h1>{post.title}</h1>
{/* 文章内容,静态,服务端渲染 */}
<div>{post.content}</div>
{/* 评论区域,动态交互,作为客户端组件 */}
<Comments postId={post.id} />
</article>
);
}
// app/blog/[slug]/Comments.js
'use client'; // 标记为客户端组件
import { useState } from 'react';
export default function Comments({ postId }) {
const [comments, setComments] = useState([]);
// 在客户端获取和操作评论数据
// ...
return <div>{/* 评论UI */}</div>;
}
这种方式让bundle size更小,性能更优,是未来的方向。但心智模型上需要适应,要清楚哪些逻辑在服务端,哪些在客户端。
4.3 性能监控与优化建议
选对模式只是第一步,上线后还要持续观察。
- 关注核心指标:LCP(最大内容绘制)、FID(首次输入延迟)、CLS(累积布局偏移)。SSG/SSR通常对LCP有益。
- 使用Next.js Analytics:Next.js自带的分析功能能帮你了解真实的页面性能(基于真实用户数据)。
- SSR服务的缓存:对于SSR页面,即使数据实时,渲染结果也可以缓存很短时间(如1-5秒),用Nginx或Next.js自身的缓存头,能大幅降低服务器压力。
- 图片优化:务必使用Next.js的
<Image>组件,它自动处理图片的响应式、懒加载和WebP格式转换,对性能提升显著。
选择渲染模式没有银弹,它是在用户体验、开发成本、服务器开销、SEO需求之间做权衡。我的经验是,对于新项目,可以从SSG开始(App Router默认),遇到动态需求再逐步引入SSR和客户端组件。多思考数据的更新频率和用户的使用场景,你的技术选型就不会偏离太远。记住,最好的模式是那个最适合你当前业务场景的模式,而不是听起来最酷的那个。

1650

被折叠的 条评论
为什么被折叠?



