当团队对技术方案争论不休
据统计,页面加载时间每增加1秒,用户流失率增加7%,转化率下降4.42%。在一次技术选型会议上,我遇到了这样的困境...
产品经理:"我要 SEO 好,首屏快!"
后端同事:"我要服务器压力小!"
前端同事:"我要开发效率高!"
测试同事:"我要兼容性好!"
老板:"我要成本低,上线快!"
作为前端技术负责人,面对既要又要还要,只好说:绝对不行好的好的,我可以的。
其实这些都和前端渲染技术息息相关。今天我们就来深入聊聊。
想象一下,你是一个用户:
- CSR网站就像定制餐厅——需要等待厨师现场制作
- SSR网站就像快餐店——点餐后立刻拿到做好的食物
- SSG网站就像便利店——提前准备好,随时可以拿走
一、CSR(客户端渲染)- 客户端现场定制
🎯 适用场景:后台管理、数据可视化、交互密集型应用
想象一下,CSR就像去一家定制餐厅,你坐下后服务员给你一张空菜单,然后厨师根据你的选择现场制作菜品。虽然等待时间较长,但可以完全按照你的口味定制。
CSR 就是“页面内容完全由浏览器端 JS 渲染,服务器只返回一个空壳 HTML 文件 和 JS 脚本”。
flowchart LR
A[用户请求页面] --> B[服务器返回空HTML+JS]
B --> C[服务器返回空HTML+JS]
C --> D[JS渲染页面内容]
代码示例
<!-- 服务器返回的HTML -->
<!DOCTYPE html>
<html>
<head>
<title>My App</title>
</head>
<body>
<!-- 空的容器 -->
<div id="app"></div>
<!-- 加载所有应用逻辑的JS -->
<script src="app.js"></script>
</body>
</html>
// app.js - 客户端执行所有渲染逻辑
document.addEventListener('DOMContentLoaded', function() {
// 客户端获取数据
fetch('/api/content')
.then(response => response.json())
.then(data => {
// 在浏览器中渲染内容
document.getElementById('app').innerHTML = `
<h1>${data.title}</h1>
<p>${data.content}</p>
`;
});
});
上述代码的工作流程如下:
1、浏览器首先加载空的HTML结构,其中只包含一个空的 <div id="app"></div> 容器
2、然后下载并执行app.js脚本
3、app.js中的代码通过fetch('/api/content')获取数据
4、最后通过document.getElementById('app').innerHTML将内容渲染到页面上
关键点:服务器只提供空壳,所有内容生成在客户端完成。搜索引擎爬虫看到的是空内容,不利于SEO。
实际案例
这里举例 www.npmcharts.com/
浏览器发起的第一个请求是获取HTML文件,在返回的结果中仅看到 id=root 的 div 标签,其他内容都由JS渲染。
在低性能或弱网环境下,页面会出现长时间白屏的情况。因为初始加载的HTML中为空壳,JS渲染内容需要一定时间。
💡 小贴士:使用骨架屏可以有效改善CSR的白屏问题
二、SSR(服务端渲染)-服务器先做好再上菜
🎯 适用场景:内容型网站、博客、新闻门户、电商首页
想象一下,SSR就像去快餐店点餐,你一下单,后厨立刻开始制作,然后直接把做好的食物端给你。虽然不能像定制餐厅那样个性化,但速度很快,而且搜索引擎也能"看到"食物的样子。
SSR 就是“服务器先帮你把页面内容生成好,浏览器拿到就能直接看到内容,然后再变成可交互的页面”。
flowchart LR
A[用户请求页面] --> B[服务器渲染HTML]
B --> C[返回完整HTML]
C --> D[浏览器显示内容]
D --> E[JS水合,页面可交互]
代码示例
// 服务端代码
async function renderPage(request) {
// 服务端获取数据
const data = await fetchDataFromAPI();
// 服务端生成完整HTML
const html = `
<!DOCTYPE html>
<html>
<head>
<title>My App</title>
</head>
<body>
<div id="app">
<h1>${data.title}</h1>
<p>${data.content}</p>
</div>
<!-- 包含客户端交互逻辑 -->
<script src="app.js"></script>
</body>
</html>
`;
return html;
}
上述代码的工作流程如下:
1、用户发起页面请求到服务器
2、服务器执行renderPage函数处理请求
3、通过fetchDataFromAPI函数从数据库或API获取页面所需数据
4、服务器使用获取的数据生成完整的HTML字符串
5、服务器将完整的HTML响应返回给浏览器
6、浏览器接收到完整HTML后立即渲染显示内容
7、加载并执行app.js进行"水合"(hydration)使页面具备交互能力
关键点:页面内容在服务器端生成,浏览器直接获得完整内容,有利于SEO和首屏性能。但每次请求都需要服务器处理,增加服务器负担。
实际案例
我们看下掘金的页面,同样在页面第一个获取HTML文件的响应中,可以找到页面的全部内容。
三、SSG(静态站点生成)- 提前做好放货架上
🎯 适用场景:博客、文档网站、企业官网、产品介绍页
SSG就像便利店,所有商品都是提前准备好放在货架上的,顾客来了直接拿走就行。速度最快,而且不需要现做,但更新需要重新"补货"。
SSG 就是“在项目构建时就把所有页面生成成静态 HTML 文件,用户访问时直接拿现成的页面”。
flowchart LR
A[构建时] --> B[服务器预渲染所有页面为HTML]
B --> C[部署到CDN]
D[用户请求页面] --> C
C --> E[CDN返回静态HTML]
代码示例
// 构建时执行
async function generateStaticPages() {
// 构建时获取数据
const allData = await fetchAllDataFromAPI();
// 为每个页面生成静态HTML文件
allData.forEach(data => {
const html = `
<!DOCTYPE html>
<html>
<head>
<title>${data.title}</title>
</head>
<body>
<div id="app">
<h1>${data.title}</h1>
<p>${data.content}</p>
</div>
<script src="app.js"></script>
</body>
</html>
`;
// 保存为静态文件
saveToFile(`./dist/${data.id}.html`, html);
});
}
上述代码的工作流程如下:
1、在项目构建阶段执行generateStaticPages函数
2、通过fetchAllDataFromAPI函数获取所有页面所需的数据
3、为每个数据项生成独立的静态HTML文件
4、将生成的HTML内容保存到指定的文件路径中
5、构建完成后将静态文件部署到CDN或Web服务器
6、用户访问时,直接从CDN或服务器返回预生成的静态HTML文件
7、浏览器加载静态HTML文件并立即显示内容
关键点:页面在构建时预生成,运行时直接提供静态文件,访问速度快且服务器压力小。但内容更新需要重新构建和部署。
实际案例
如 Vue官方文档 采用了 SSG技术(基于vitepress框架),内容固定,访问量大。
💡 小贴士:结合CDN使用,可以让全球用户都享受到"就近取货"的极速体验
让我们回顾一下 CSR、SSR、SSG的区别
单一渲染技术对比
| 渲染技术 | 首屏速度 | SEO友好度 | 服务器压力 | 客户端性能 | 实时性 | 适用场景 | 学习成本 |
|---|---|---|---|---|---|---|---|
| CSR | 🔴 低 | 🔴 低 | 🟢 低 | 🟡 中 | 🟢 高 | 后台管理、交互密集型应用 | 🟢 简单 |
| SSR | 🟢 快 | 🟢 好 | 🔴 高 | 🟡 中 | 🟢 高 | 内容网站、电商、新闻门户 | 🔴 复杂 |
| SSG | 🟢 快 | 🟢 好 | 🟢 低 | 🟢 好 | 🔴 低 | 博客、文档、官网 | 🟡 中等 |
通过上述对比,我们发现三种技术各有优势和不足。在企业大型的复杂应用中,常灵活组合使用上述三种技术,以获得最佳性能。下面让我们详细介绍4种混合渲染技术。
四、ISR(增量静态再生)- 卖完再补货的便利店
🎯 适用场景:内容频繁更新但可以接受短暂延迟的网站
ISR就像智能便利店,热门商品提前准备好,但当商品快卖完或有新品时,系统会自动补货。既保证了大部分时间的快速访问,又能及时更新内容。
ISR 就是“页面首次访问时生成静态 HTML,后续定期或按需自动再生,兼顾静态站点的速度和动态内容的灵活”。实际上是 SSG 和 SSR 的混合。
flowchart LR
A[用户首次请求页面] --> B[服务器生成静态HTML]
B --> C[缓存HTML到CDN]
D[后续用户请求] --> C
C --> E[CDN返回静态HTML]
F[内容变更或定时触发] --> G[重新生成HTML]
G --> C
代码示例
// ISR核心概念演示
class ISRPageCache {
constructor() {
this.cache = new Map();
this.isGenerating = new Set();
}
async getPage(path) {
const cached = this.cache.get(path);
// 1. 如果有缓存且未过期,直接返回
if (cached && Date.now() - cached.timestamp < 60000) { // 1分钟缓存
return cached.content;
}
// 2. 如果正在生成,返回旧内容(如果有的话)
if (this.isGenerating.has(path)) {
return cached ? cached.content : "Loading...";
}
// 3. 开始生成新内容
this.isGenerating.add(path);
try {
const content = await this.generatePageContent(path);
const entry = {
content,
timestamp: Date.now()
};
this.cache.set(path, entry);
return content;
} finally {
this.isGenerating.delete(path);
}
}
async generatePageContent(path) {
// 模拟从API获取数据
const data = await fetchDataFromAPI(path);
return `<h1>${data.title}</h1><p>${data.content}</p>`;
}
}
// 使用示例
const isrCache = new ISRPageCache();
// 首次访问 - 生成页面
app.get('/post/:id', async (req, res) => {
const html = await isrCache.getPage(`/post/${req.params.id}`);
res.send(html);
});
上述代码的工作流程如下:
1、用户请求一个页面路径(如/post/123)
2、ISRPageCache检查是否有该路径的有效缓存内容
3、如果有未过期的缓存,直接返回缓存内容给用户
4、如果没有缓存但正在后台生成,则返回旧内容或加载占位符
5、如果既没有有效缓存也没有正在生成,则开始生成新内容
6、调用generatePageContent函数获取最新数据并生成HTML内容
7、将新生成的内容存入缓存并设置时间戳
8、返回生成的HTML内容给用户
关键点:结合了SSG的速度优势和SSR的实时性,通过智能缓存机制平衡了性能和内容新鲜度。适合内容更新频率适中的网站。
五、Suspense for SSR(流式渲染)- 边做边上的套餐
🎯 适用场景:复杂页面、需要极致首屏速度的网站
流式渲染就像餐厅的套餐服务,服务员可以先把已经做好的前菜端上来,主菜还在制作中。用户可以先看到部分内容,不用等待整个页面完成。
Suspense for SSR 就是“服务器可以一边生成页面一边把内容分批发给浏览器,用户能更快看到页面的部分内容”。
flowchart LR
A[服务器开始渲染] --> B[部分HTML生成即发送]
B --> C[浏览器逐步接收并渲染]
C --> D[异步数据准备好后插入剩余内容]
代码示例
// 流式渲染概念演示
async function* renderStream() {
// 立即发送头部
yield '<!DOCTYPE html><html><body>';
yield '<header><h1>网站头部 (立即显示)</h1></header>';
// 开始获取慢数据,同时发送占位符
yield '<main id="content"><div>加载中...</div></main>';
// 模拟异步获取数据
const slowData = await fetchSlowData(); // 假设这需要3秒
// 发送实际内容(替换占位符)
yield `<script>
document.getElementById('content').innerHTML =
'<h2>${slowData.title}</h2><p>${slowData.content}</p>';
</script>`;
yield '</body></html>';
}
// 使用示例
app.get('/streaming-page', async (req, res) => {
res.setHeader('Content-Type', 'text/html; charset=utf-8');
for await (const chunk of renderStream()) {
res.write(chunk);
// 模拟网络传输延迟
await new Promise(resolve => setTimeout(resolve, 10));
}
res.end();
});
上述代码的工作流程如下:
1、用户请求流式渲染页面(/streaming-page)
2、服务器设置响应头Content-Type为text/html
3、renderStream生成器函数开始执行,立即生成并发送HTML头部和页面骨架
4、浏览器接收到部分内容并立即开始渲染显示
5、服务器同时开始异步获取需要较长时间的数据(fetchSlowData)
6、数据获取完成后,服务器发送JavaScript代码片段来更新页面内容
7、浏览器执行JavaScript,将占位符替换为实际内容
8、服务器发送HTML结束标签,完成整个响应过程
关键点:通过流式传输,用户可以更快地看到页面部分内容,改善感知加载速度。特别适用于页面包含耗时操作的场景。
六、RSC(React Server Components,服务器组件)- 服务器代劳减负
RSC就像请了一个专门的"服务器助手",帮你处理复杂的数据获取和计算工作,而客户端只需要处理交互部分。大大减少了客户端的负担。
RSC 就是“部分 React 组件只在服务器端渲染,客户端无需加载相关 JS,极大减小前端体积”。
flowchart LR
A[服务器渲染RSC组件] --> B[只返回HTML/数据]
B --> C[客户端渲染普通组件]
C --> D[最终合成完整页面]
代码示例
// RSC核心概念演示
// 服务器组件 - 不会发送到客户端
async function ServerComponent() {
// 可以直接访问数据库,无需API调用
const data = await db.query("SELECT * FROM posts");
// 可以执行重型计算,不占用客户端资源
const expensiveResult = performHeavyComputation(data);
// 返回轻量级数据,而非组件代码
return {
type: 'ServerComponentResult',
props: {
posts: data,
computed: expensiveResult
}
};
}
// 客户端组件 - 会发送到客户端
function ClientComponent({ posts, computed }) {
const [count, setCount] = useState(0);
return (
<div>
<button onClick={() => setCount(count + 1)}>
Count: {count}
</button>
<ul>
{posts.map(post => (
<li key={post.id}>{post.title}</li>
))}
</ul>
<p>Computed: {computed}</p>
</div>
);
}
// 混合渲染示例
async function renderApp() {
// 服务器组件在服务端执行
const serverResult = await ServerComponent();
// 结果和客户端组件一起发送到浏览器
return `
<!DOCTYPE html>
<html>
<body>
<div id="app"></div>
<script>
// 服务器组件的结果作为数据传递
window.__SERVER_DATA__ = ${JSON.stringify(serverResult.props)};
// 客户端组件代码
function ClientComponent({ posts, computed }) {
// 客户端交互逻辑
// (简化版,实际使用React)
return '<div>' + posts.map(p => '<p>' + p.title + '</p>').join('') + '</div>';
}
// 渲染混合结果
document.getElementById('app').innerHTML =
ClientComponent(window.__SERVER_DATA__);
</script>
</body>
</html>
`;
}
上述代码的工作流程如下:
1、用户请求页面时,服务器执行renderApp函数开始处理请求
2、在服务器上执行ServerComponent组件,直接访问数据库并执行重型计算
3、ServerComponent只返回计算结果数据,而不是组件代码本身
4、服务器将客户端组件代码和服务器组件的结果数据一起打包到HTML中
5、浏览器加载HTML,执行客户端JavaScript代码
6、客户端组件使用服务器组件提供的数据进行渲染
7、用户界面显示完整内容,并具备交互能力
关键点:将数据获取和重型计算放在服务器端执行,减少了客户端JavaScript包的大小,提高了页面加载性能。
💡 小贴士:服务器组件不能使用浏览器API,需要合理划分组件边界
七、PPR(Partial Prerendering,部分预渲染)- 混合式智能餐厅
PPR就像一家智能餐厅,固定菜单提前准备好,特色菜品现场制作。既保证了常用菜品的快速供应,又能提供个性化服务。
PPR 就是“页面的部分内容在构建时预渲染,部分内容在运行时动态渲染,实现更细粒度的性能优化”。
flowchart LR
A[构建时] --> B[预渲染部分页面]
C[运行时] --> D[动态渲染剩余部分]
B & D --> E[最终合成完整页面]
代码示例
// PPR核心概念演示
class PartialPrerenderer {
constructor() {
// 预渲染的静态部分
this.staticShell = `
<!DOCTYPE html>
<html>
<head><title>PPR示例</title></head>
<body>
<header><h1>网站头部 (预渲染)</h1></header>
<main id="dynamic-content">
<!-- 动态内容占位符 -->
<div id="skeleton">加载中...</div>
</main>
<script>
// 客户端激活代码
async function loadDynamicContent() {
const res = await fetch('/api/dynamic-data');
const data = await res.json();
document.getElementById('dynamic-content').innerHTML =
'<h2>' + data.title + '</h2><p>' + data.content + '</p>';
}
loadDynamicContent();
</script>
</body>
</html>
`;
}
getPrerenderedShell() {
return this.staticShell;
}
// 构建时预渲染静态部分
async buildStaticShell() {
// 只渲染静态部分,动态部分留空
return this.staticShell;
}
}
const ppr = new PartialPrerenderer();
// 构建时
// const shell = await ppr.buildStaticShell();
// fs.writeFileSync('dist/index.html', shell);
// 运行时
app.get('/', (req, res) => {
// 立即返回预渲染的静态外壳
res.send(ppr.getPrerenderedShell());
});
上述代码的工作流程如下:
1、在构建阶段,执行buildStaticShell方法生成页面的静态外壳
2、静态外壳包含页面的基本结构和静态内容(如头部)
3、动态内容区域仅包含占位符和客户端获取数据的脚本
4、构建产物(静态外壳HTML)部署到服务器或CDN
5、用户请求页面时,服务器立即返回预渲染的静态外壳
6、浏览器加载页面并显示静态内容和占位符
7、客户端JavaScript执行,向/api/dynamic-data发起请求获取动态数据
8、获取数据后,通过DOM操作将动态内容插入到指定位置
关键点:结合了SSG的快速响应和SSR的动态内容能力,通过预渲染静态部分实现快速首屏显示,同时保留动态内容的实时性。这是一种更细粒度的优化策略。
💡 小贴士:PPR是Next.js的前沿特性,目前还在实验阶段,适合尝鲜项目。目前仅 Next.js Canary 版本支持,API 仍在演进中。
让我们回顾一下上面4种混合渲染技术
混合渲染技术全方位对比
| 渲染技术 | 首屏速度 | SEO友好度 | 服务器压力 | 客户端性能 | 实时性 | 适用场景 | 学习成本 |
|---|---|---|---|---|---|---|---|
| ISR | 🟢 快 | 🟢 好 | 🟡 中 | 🟢 好 | 🟡 中 | 内容频繁更新的网站 | 🔴 复杂 |
| 流式渲染 | 🟢 快 | 🟢 好 | 🟡 中 | 🟢 好 | 🟢 高 | 复杂页面、追求极致性能 | 🔴 复杂 |
| RSC | 🟢 快 | 🟢 好 | 🟢 低 | 🟢 好 | 🟡 中 | React项目、性能要求极高 | 🔴 复杂 |
| PPR | 🟢 快 | 🟢 好 | 🟡 中 | 🟢 好 | 🟢 高 | 混合内容、复杂场景 | 🔴 复杂 |
技术选型决策
当我们拿到一个新项目时,应该如何选择前端渲染技术呢?这里给大家一些建议:
1.【首要原则】框架生态决定可用选项
-
在讨论渲染模式之前,团队的技术栈(React, Vue, Svelte 等)是首要的决定因素。你只能在你选择的生态圈里做决策。
-
React 生态 :如果你使用 React, Next.js 是事实上的“全能”框架,它提供了最全面的渲染支持,包括 CSR, SSG, SSR, ISR, 流式渲染, RSC 和 PPR。
-
Vue 生态 :如果你使用 Vue, Nuxt.js 是对应的解决方案,它完美支持 CSR, SSG, SSR 和 ISR。但需要明确,像 RSC, PPR 这类前沿技术 目前是 React/Next.js 生态独有的 。
-
其他生态 :Svelte 有 SvelteKit ,Angular 有 Angular Universal ,它们也提供了各自的 SSR/SSG 方案。
2.默认选项:客户端渲染 (CSR)
- 在你选定的框架内,如果项目 不需要 SEO ,那么标准的 CSR 就是最简单、最高效的开发模式。
3.SEO 决定 是否选用服务端渲染
- 一旦需要 SEO,你就必须选择一个支持服务端渲染的元框架(如 Next.js, Nuxt.js)。这是你从“纯前端”迈向“全栈”渲染的标志。
4.内容是否变化 决定 SSG vs. SSR
- 在 Next.js/Nuxt.js 等框架中,根据页面内容来选择:
- 内容几乎不变(博客、文档):用 SSG 。
- 内容因人而异、实时变化(用户中心、搜索页):用 SSR 。
5.高级模式用于解决特定瓶颈
只有当基础的 SSG/SSR 出现问题时,才在你的框架生态中寻找对应的解决方案。
- SSG 构建慢/内容需准确实时 -> 查找框架的 ISR 功能 (Next.js, Nuxt.js 都支持)。
- SSR 首屏慢 -> 查找框架的 流式渲染 支持 (Next.js, Nuxt.js 都支持)。
- 页面大部分静态,仅少量动态 -> 如果你在用最新的 Next.js,可以考虑 PPR 。
6.将 RSC 视为架构演进,而非渲染模式
React Server Components (RSC) 是一种思考组件的新方式,它让组件可以在服务端运行,且无需将 JavaScript 发送到客户端。它可以和 SSG/SSR/ISR 等模式结合使用,以减少客户端包体积、提升性能。当项目组件逻辑变得非常复杂时,可以考虑引入它作为架构优化手段。
展望
1.边缘渲染 (Edge Rendering)
在CDN节点直接渲染,降低延迟
代表技术:Cloudflare Workers, Vercel Edge Functions
优势:全球用户就近访问,降低中心服务器负载
2.AI辅助渲染优化
智能预测用户行为,预加载内容
根据设备性能自动选择渲染策略
动态调整渲染优先级
总结:没有银弹,只有最适合
🎯 核心要点回顾
前端渲染技术的选择不是非黑即白的,而是在性能、SEO、开发效率、服务器成本等多个维度上的权衡:
CSR - 简单直接,适合交互密集型应用
SSR - 平衡之选,适合内容型网站
SSG - 性能之王,适合静态内容站点
ISR - 智能更新,适合内容频繁变化的网站
流式渲染 - 极致体验,适合复杂页面
RSC - 性能革命,适合React项目
PPR - 精细控制,适合混合场景
💡 选择建议
记住这句口诀:
"静态用SSG,动态选SSR,频繁更新ISR,交互密集CSR,追求极致RSC"
🚀 行动起来
评估你的项目需求:使用我们决策建议
从小处着手:先在小功能上尝试新技术
持续优化:监控性能指标,持续改进
关注社区:前端技术日新月异,保持学习
📚 进一步学习资源
- React Server Components RFC - 深入了解RSC
- Nuxt.js官方文档 - 掌握Vue生态渲染方案
- Next.js官方文档 - 学习现代化React渲染技术
- vitepress官方文档 - 快速搭建SSG静态文档网站
- ISR - Next.js官方文档
- Streaming with React Suspense - Next.js官方文档
- PPR - Next.js官方文档
- 万字长文:深度解析React渲染技术演进之路
- Next.js Canary支持部分预渲染以实现更快的网站
🤝 互动时间
💬 你在项目中遇到过哪些渲染相关的挑战?
🤔 你最想深入了解哪种渲染技术?
🌟 如果这篇文章对你有帮助,请点赞收藏分享!
作者寄语:技术没有绝对的好坏,只有适不适合。希望这篇文章能帮助你在前端渲染的技术海洋中找到属于你的那座灯塔。记住,最好的架构是演化出来的,而不是设计出来的。让我们一起在实践中探索,在探索中成长!
🔗 下期预告:《Nuxt.js入门指南-Vue生态下的高效渲染技术》
🔔 关注我,第一时间获取前端技术干货!