引言
最近在工作中,我发现一些同事不太了解前端的服务端渲染(SSR)技术。 作为一个接触开发过多个服务端渲染项目的开发者,我决定写这篇文章,跟大家介绍下服务端渲染技术的前世今生。 希望通过这篇文章,能帮助没有接触过的同学理解什么是 SSR、为什么要用 SSR,以及在实际项目中如何选择合适的技术方案。
1. 最早的服务端渲染 - JSP 时代
说到服务端渲染,我们得先聊聊最早的 JSP/PHP 时代。那时候还没有前后端分离的概念,前端代码直接写在后端页面里面。先看看它的工作流程:
sequenceDiagram
participant Client as 浏览器
participant Server as 服务器(JSP)
participant Database as 数据库
Client->>Server: 发送请求
Server->>Database: 查询数据
Database-->>Server: 返回数据
Server->>Server: 生成 HTML
Server-->>Client: 返回完整 HTML
那时候的代码是这样的:
<%@ page language="java" contentType="text/html; charset=UTF-8" pageEncoding="UTF-8"%>
<%@ page import="java.sql.*" %>
<%@ taglib uri="<http://java.sun.com/jsp/jstl/core>" prefix="c" %>
<!DOCTYPE html>
<html>
<head>
<title>商品列表</title>
</head>
<body>
<h1>商品列表</h1>
<%
// 直接在页面里写数据库查询
Class.forName("com.mysql.jdbc.Driver");
Connection conn = DriverManager.getConnection(
"jdbc:mysql://localhost:3306/store", "user", "password");
Statement stmt = conn.createStatement();
ResultSet rs = stmt.executeQuery("SELECT * FROM products");
request.setAttribute("products", rs);
%>
<ul>
<c:forEach var="product" items="${products}">
<li>
${product.name} - ${product.price}元
<form action="addToCart.jsp" method="POST">
<input type="hidden" name="productId" value="${product.id}">
<input type="submit" value="加入购物车">
</form>
</li>
</c:forEach>
</ul>
</body>
</html>
看到这段代码,有写过 JSP 的同学可能会感到亲切。没错,那时候的开发就是这样,HTML、Java 代码、数据库查询都混在一起。这种开发方式的好处是:
- 首屏速度快,用户打开页面就能看到内容
- 对搜索引擎特别友好,因为返回的是完整的 HTML
- 不需要写额外的前端代码,后端开发能直接上手
但问题也很明显:
- 前后端代码严重耦合,改个样式可能要找后端同学
- 每次点击链接或提交表单,整个页面都要刷新
- 开发效率低,前后端同学经常要改同一个文件
- 代码难以维护,一个文件要身兼数职
这种开发方式随着前端技术的发展,逐渐显露出了它的局限性。特别是在 Ajax 技术出现后,前端开发开始走向了一个新的时代。
2. 单页应用时代 - 前后端分离来了
随着 Ajax 技术的普及,以及 React、Vue 这些前端框架的出现,我们的开发方式发生了翻天覆地的变化。前端再也不用依赖后端模板,可以独立写业务逻辑了。我们先看看单页应用的工作流程:
sequenceDiagram
participant Client as 浏览器
participant Server as 服务器
participant API as API 服务
Client->>Server: 请求 HTML/JS/CSS
Server-->>Client: 返回静态资源
Client->>Client: 执行 JS,初始化应用
Client->>API: AJAX 请求数据
API-->>Client: 返回 JSON 数据
Client->>Client: 更新页面内容
Note over Client: 路由变化
Client->>API: 请求新页面数据
API-->>Client: 返回新数据
Client->>Client: 更新视图
让我们看看现在最常见的单页应用是怎么写的:
// React 组件示例
function ProductList() {
const [products, setProducts] = useState([]);
// 页面加载时获取商品数据
useEffect(() => {
fetch('/api/products')
.then(res => res.json())
.then(data => setProducts(data));
}, []);
return (
<ul>
{products.map(product => (
<li key={product.id}>
{product.name} - ¥{product.price}
<button onClick={() => addToCart(product.id)}>
加入购物车
</button>
</li>
))}
</ul>
);
}
对比一下之前的 JSP 代码,是不是清爽多了?前端只需要关注页面展示和交互,数据通过调用 API 接口获取。这种开发方式我们称之为 SPA(单页应用)或者 CSR(客户端渲染)。
这种模式最大的特点是前后端完全分离:
- 前端写界面和交互,通过 API 获取数据
- 后端专注写接口,不用管页面长啥样
- 部署时前端静态文件和后端接口服务可以分开部署
用过 SPA 的同学都知道它的好处:
- 用户体验好,点击按钮直接更新数据,不用刷新整个页面
- 前后端并行开发,提高效率
- 代码结构清晰,方便维护
- 一套接口可以供多端使用(网页、手机 App 等)
但实际用下来,我们也发现了一些问题:
- 首屏加载速度慢,因为要等 JS 加载完并执行才能看到页面
- 搜索引擎不友好,爬虫抓取不到实际内容
- 如果用户网络差或者用的是低配手机,体验会比较糟糕
特别是第一个问题 —— 首屏加载慢,这个在电商项目里特别明显。用户打开页面要等好几秒才能看到商品列表,这显然是不能接受的。
3. 现代 SSR 的崛起
为了解决 SPA 的这些问题,现代服务端渲染技术应运而生。它的核心思想是:既然客户端渲染和服务端渲染各有优势,那么能不能把两者结合起来?
于是就有了现在的解决方案:
- 首次访问时使用服务端渲染,让用户快速看到内容
- 后续交互使用客户端渲染,保证体验流畅
让我们看看它是怎么工作的:
sequenceDiagram
participant Client as 浏览器
participant Server as SSR 服务器
participant API as 后端 API
Client->>Server: 访问页面
Server->>API: 获取数据
API-->>Server: 返回数据
Server->>Server: 生成 HTML
Server-->>Client: 返回 HTML 和 JS
Note over Client: 页面立即显示
Client->>Client: 加载 JS(水合过程)
Note over Client: 页面可交互
这种模式引入了一个重要的概念 —— "水合"(Hydration)。它的意思是:
- 服务端生成 HTML 发给浏览器(就像给一具躯体)
- 浏览器加载并执行 JS(给躯体注入灵魂)
- 页面变得可交互(躯体活了过来)
举个简单的例子:
// 一个通用的 SSR 组件写法
function ProductList({ products }) {
// 这段代码既能在服务端运行,也能在客户端运行
return (
<ul>
{products.map(product => (
<li key={product.id}>
{product.name} - ¥{product.price}
<button onClick={() => addToCart(product.id)}>
加入购物车
</button>
</li>
))}
</ul>
);
}
现代 SSR 的优点是:
- 首屏渲染快,用户体验好
- SEO 友好,搜索引擎能抓取到内容
- 后续交互流畅,和 SPA 一样
- 可以同时兼顾性能和体验
但它也有一些缺点:
- 开发复杂度高
- 要考虑服务端和客户端的代码兼容性
- 有些浏览器 API 在服务端不能用
- 调试起来比较麻烦
- 部署相对麻烦
- 需要一个 Node.js 服务器来跑 SSR
- 要考虑服务器缓存策略
- 运维成本比纯静态文件高
- 服务器压力大
- 每个页面请求都要在服务器执行渲染
- 并发量大时需要考虑扩容
- 学习成本高
- 要了解更多的概念(比如 hydration)
- 框架规范比较多,限制比较多
4. 怎么选择适合自己的方案?
在实际项目中到底该怎么选择呢?我总结了一下经验:
适合用传统服务端渲染(JSP/PHP)的场景:
- 小项目,功能简单,没什么交互
- 团队以后端为主,不太懂前端技术
- 对性能要求不高的内部系统
适合用 SPA 的场景:
- 交互复杂的后台管理系统
- 不需要考虑 SEO
- 用户主要是电脑访问
- 对首屏加载速度要求不高
适合用现代 SSR 的场景:
- 电商网站、新闻网站等需要 SEO 的项目
- 对首屏加载速度要求高的网站
- 有一定的前端开发能力
- 用户设备情况复杂,需要照顾低端设备
5. 未来的发展趋势
说到 SSR 的未来,最近几年又出现了一些新的概念和技术:
- Islands 架构 这是 Astro 框架带火的概念,它的想法是:页面中不是所有部分都需要交互,那为什么要给所有内容都加上 JavaScript 呢?它把页面分成:
- 静态区域:纯 HTML,不需要 JS
- 交互区域:需要 JS 的部分
这样可以大大减少加载的 JS 量,提升性能。
- 边缘渲染 随着云服务的发展,现在可以在离用户最近的节点进行渲染,这样可以大大减少响应时间。比如 Vercel 的边缘渲染就是这个思路。
- 混合渲染 不同页面用不同的渲染方式:
- 经常变化的页面用 SSR
- 较少变化的页面用静态生成
- 纯静态的内容直接部署 HTML
结语
回顾服务端渲染技术的发展历程,我们可以看到它是在不断解决实际问题中演进的。从最早的 JSP 到现代 SSR,每种技术都有其适用场景,关键是要结合自己的项目需求和团队能力来选择。
随着前端技术的发展,我相信会有更多新的解决方案出现。作为开发者,我们需要:
- 了解不同技术方案的优缺点
- 保持学习新技术的热情
- 在实践中不断总结经验