React应用默认在浏览器端完成渲染,这意味着用户拿到的是一份几乎空白的HTML外壳,再由JavaScript接管页面生成。这种方式对交互型后台没问题,但面对内容页、商品页这类需要SEO和首屏速度的场景就力不从心了。React生态给出的答案是预渲染,也就是在请求到达之前或请求发生时,由服务端先产出一份完整的HTML。预渲染主要分两条路线:SSR(服务器端渲染)在每次请求时动态生成HTML,SSG(静态站点生成)则在构建阶段一次性生成所有HTML文件。实际项目中,一个站点往往同时存在静态内容和动态内容,单一策略很难覆盖全部需求,因此SSR与SSG的混合使用成为React工程中的常见实践。

SSR与SSG的核心差异在哪里
要理解混合策略,先要弄清两种渲染方式的本质区别。SSG发生在构建时刻,构建脚本会遍历所有可能的页面路径,把每个页面渲染成独立的HTML文件,部署后这些文件可以直接放在CDN上,用户请求时几乎零计算开销。SSR则发生在请求时刻,服务器收到请求后执行React组件的渲染逻辑,可能还要去数据库或接口拉取最新数据,再把拼好的HTML返回给浏览器。
两者的差异直接体现在三个维度。第一是数据时效性:SSG的页面内容在构建时就固定了,数据更新需要重新构建;SSR每次请求都能拿到最新数据。第二是服务器压力:SSG部署后是纯静态资源,可以用任何静态服务器托管;SSR需要常驻的Node.js进程,请求量大时要考虑扩容。第三是首屏性能:SSG配合CDN能做到极快的响应,SSR虽然省去了客户端请求数据的等待,但渲染本身消耗服务器时间,高并发下反而可能变慢。
用一个简单的类比来理解:SSG像提前印好的报纸,印刷完成那一刻内容就定了,分发速度极快;SSR像现场打印的定制报告,内容永远最新,但每一份都要现做。理解了这一点,混合策略的思路就清晰了——把页面按内容更新频率分类,静态的走SSG,动态的走SSR,处于中间状态的用增量再生来平衡。
如何判断页面该用哪种渲染策略
选择渲染策略的核心依据是数据变化频率和个性化程度。对于营销首页、帮助文档、博客文章这类内容,更新频率低且对所有用户一致,SSG是毫无疑问的最优解。构建一次,全球CDN分发,首屏性能和SEO都能拉满。而对于用户个人中心、购物车、实时行情这类强个性化或强实时页面,SSR是唯一选择,因为构建时根本无法预知每个用户会看到什么。
真正麻烦的是中间地带。比如电商商品详情页,全站可能有几百万个SKU,构建时全部静态化会导致构建时间长达数小时,但如果全部走SSR,服务器又要承担巨大的渲染压力。再比如新闻资讯站,列表页需要相对及时,但也不必精确到秒级。针对这类场景,Next.js提供了两个折中手段:增量静态再生(ISR)允许静态页面在指定时间间隔后自动重新生成,以及动态参数的fallback模式,让未被静态化的路径在首次请求时按需生成,之后复用静态缓存。
一个实用的判断流程是:先问页面数据是否与用户身份相关,相关则SSR;再问页面数量是否可控且更新频率是否低于每日级别,是则纯SSG;如果页面数量庞大或更新频繁但不需要每次都最新,用ISR设置合理的再验证时间;只有确实要求每次请求都是最新数据的页面,才落到SSR。这套分层能覆盖绝大多数业务场景。
Next.js中混合渲染的代码实践
下面通过一个电商站点的例子展示混合渲染的写法。商品列表页数据更新不频繁,用ISR每60秒再生一次;商品详情页数量庞大,用fallback按需静态化;用户订单页强个性化,走SSR。
// pages/products/index.js
// 商品列表页:增量静态再生,60秒后后台刷新
export async function getStaticProps() {
const products = await fetch('https://api.ipipp.com/products?limit=20')
.then(res => res.json());
return {
props: { products },
revalidate: 60, // 60秒内返回缓存,之后首次请求触发后台重新生成
};
}
// pages/products/[id].js
// 商品详情页:只预渲染部分热门商品,其余按需生成
export async function getStaticPaths() {
const hotProducts = await fetch('https://api.ipipp.com/products/hot')
.then(res => res.json());
return {
paths: hotProducts.map(p => ({ params: { id: String(p.id) } })),
fallback: 'blocking', // 未预渲染的路径在请求时生成,完成后缓存为静态页
};
}
export async function getStaticProps({ params }) {
const product = await fetch(`https://api.ipipp.com/products/${params.id}`)
.then(res => res.json());
return {
props: { product },
revalidate: 300, // 长尾商品5分钟再生一次即可
};
}
// pages/orders.js
// 订单页:强个性化数据,必须走服务端渲染
export async function getServerSideProps({ req }) {
const token = req.cookies.session;
const orders = await fetch('https://api.ipipp.com/orders', {
headers: { Authorization: `Bearer ${token}` },
}).then(res => res.json());
return { props: { orders } };
}上面三段代码覆盖了混合渲染的三种典型形态。revalidate字段是ISR的关键,它声明了静态页面的有效期,过期后的第一个请求仍会拿到旧的缓存页面,同时服务器在后台重新生成新版本,这种策略保证了用户永远不会因为等待重建而卡住。fallback设为blocking时,未预渲染的路径会在请求时同步生成HTML再返回,用户看到的是完整内容而非骨架屏,SEO爬虫也能正常抓取。
需要注意SSR页面与静态页面的部署差异。getServerSideProps所在的页面要求运行环境是Node.js服务器或支持函数计算的边缘节点,不能直接扔到纯静态托管上。如果项目部署在Vercel,SSR会自动转为Serverless函数;自建服务器则需要用next start启动并做好进程守护和负载均衡。混合策略下建议把静态页面和动态页面的路由做好规划,静态的尽量收敛到独立目录,便于CDN缓存规则的配置。
混合方案的常见坑与优化建议
第一个常见的坑是ISR的缓存失效问题。当数据源发生变更时,除了等待revalidate到期,也可以主动调用revalidate接口触发指定路径的重新生成,这在商品价格修改、文章发布等场景下能实现准实时的内容更新,同时保留静态化的性能优势。
第二个坑是构建时间失控。如果商品规模持续增长,即使fallback策略也要维护热门商品的预渲染列表,建议根据访问日志动态生成getStaticPaths的返回值,只预渲染近期有流量的页面,让长尾完全依赖按需生成。同时为构建任务设置合理的并行度和超时,避免一次数据接口变慢拖垮整个构建流程。
第三个坑是水合(hydration)不一致。无论SSG还是SSR,服务端产出的HTML会在客户端被React重新接管,如果渲染逻辑依赖window、Date.now这类环境相关的值,就会出现水合警告甚至页面错乱。解决办法是把环境相关的计算放到useEffect中执行,或者利用suppressHydrationWarning处理确实无法避免的差异。把这些细节处理好,混合渲染方案才能在性能、实时性和维护成本之间拿到最好的平衡。