大前端技术学习(一):客户端&服务端渲染技术
两种渲染方式的“性格分析”
CSR:那个自由奔放的艺术家
CSR(客户端渲染)有点像一位自由艺术家——它在用户的浏览器里挥洒创意,动态地创造一切。
// 这就是CSR的核心哲学
function renderEverythingInBrowser() {
// 1. 先给用户一个空白画布
const app = document.getElementById('app');
app.innerHTML = '<div class="spinner">加载中...</div>';
// 2. 慢慢准备颜料(数据)
fetch('/api/data').then(data => {
// 3. 开始创作(渲染)
app.innerHTML = createBeautifulUI(data);
});
}
CSR的真实体验:
- 首次访问:看到loading动画 → 等待 → 页面突然完整呈现
- 后续导航:丝般顺滑,就像在用桌面应用
- 开发体验:前后端完全分离,前端可以玩得很嗨
但我记得有一次,我们的网站在Google搜索里完全找不到商品详情页。SEO团队找上门来,那场面有点尴尬。后来才知道,Google的爬虫虽然能执行JavaScript,但时间有限,我们那个页面要5秒才能渲染完,爬虫早就走了。
SSR:那位稳重的工匠
SSR(服务端渲染)更像一位老工匠,在把作品交给你之前,已经精心打磨好了每一个细节。
// SSR的工作方式
async function handleRequest(req, res) {
// 1. 收到请求后,立即开始工作
const user = await getUser(req);
const products = await getProducts();
// 2. 在服务器端就把一切都准备好
const html = `
<!DOCTYPE html>
<html>
<head><title>商品列表</title></head>
<body>
<h1>欢迎回来,${user.name}!</h1>
<div class="products">
${products.map(p => `<div>${p.name} - ¥${p.price}</div>`).join('')}
</div>
</body>
</html>
`;
// 3. 把完整的作品交付给用户
res.send(html);
}
SSR的温暖之处:
- 打开页面:内容瞬间呈现,没有“等待感”
- 分享链接:社交媒体能抓取到完整的页面预览
- 低端设备:老款手机也能快速看到内容
不过SSR也有自己的烦恼。有一次促销活动,流量暴增,我们的Node服务器因为要为每个请求渲染页面,CPU直接飙到100%,页面打开时间从200ms涨到了5秒。
现代开发者的“中庸之道”
现在的我,已经不再非黑即白地看待这个问题。就像做菜,CSR和SSR都是食材,关键看你怎么搭配。
场景一:营销落地页 → SSG
对于公司官网、产品介绍页这些内容不怎么变动的页面,我选择静态站点生成(SSG)。
// Next.js的getStaticProps
export async function getStaticProps() {
// 构建时获取数据,生成静态HTML
const pageData = await fetchPageData();
return {
props: { pageData },
// 每24小时重新生成一次
revalidate: 86400
};
}
上次我们公司官网从WordPress迁移到Next.js SSG,加载时间从2.8秒降到了0.4秒,而且再也不用担心被黑客攻击了——静态文件太安全了。
场景二:用户仪表盘 → CSR
对于需要大量交互、实时数据的后台管理系统,我坚持用CSR。
// 后台管理系统的典型结构
function AdminDashboard() {
const [realTimeData, setRealTimeData] = useState(null);
// WebSocket实时更新数据
useEffect(() => {
const ws = new WebSocket('wss://api.example.com/realtime');
ws.onmessage = (event) => {
setRealTimeData(JSON.parse(event.data));
};
}, []);
// 各种交互式图表和表单
return (
<div>
<RealTimeChart data={realTimeData} />
<InteractiveDataGrid />
<ComplexFilterPanel />
</div>
);
}
这里用SSR意义不大,因为每个用户看到的内容都不一样,而且交互极其复杂。
场景三:电商详情页 → SSR + CSR混合
这是最有趣的模式。我们为电商产品页设计的方案:
// 混合渲染示例
function ProductPage({ initialProductData }) {
// 首屏用SSR渲染的关键信息
return (
<div>
{/* SSR渲染的部分 - 立即显示 */}
<div className="product-header">
<h1>{initialProductData.name}</h1>
<img src={initialProductData.image} alt={initialProductData.name} />
<div className="price">¥{initialProductData.price}</div>
</div>
{/* 懒加载的交互部分 */}
<Suspense fallback={<div>加载评论中...</div>}>
<ProductReviews productId={initialProductData.id} />
</Suspense>
{/* 只在客户端渲染的推荐模块 */}
<ClientOnly>
<ProductRecommendations />
</ClientOnly>
</div>
);
}
// 服务端获取初始数据
export async function getServerSideProps(context) {
const productId = context.params.id;
const productData = await fetchProductData(productId);
return {
props: {
initialProductData: productData
}
};
}
这样做的好处是:
- 用户瞬间看到产品核心信息(SSR的优点)
- 评论区等次要内容慢慢加载
- 个性化推荐只在客户端计算,不增加服务器负担
实战中的那些“坑”与“解”
坑1:SSR中的window未定义
// 错误示范
function MyComponent() {
// 服务端渲染时会报错:window is not defined
const screenWidth = window.innerWidth;
return <div>宽度:{screenWidth}px</div>;
}
// 正确做法
function MyComponent() {
const [screenWidth, setScreenWidth] = useState(0);
useEffect(() => {
// 只在客户端执行
setScreenWidth(window.innerWidth);
}, []);
return <div>宽度:{screenWidth || '加载中...'}px</div>;
}
坑2:CSR的首屏空白时间太长
解决方案:
- 代码分割:让首屏只加载必要的代码
- 骨架屏:给用户一个加载预期
- 资源预加载:悄悄加载下一页的资源
// 骨架屏示例
function ProductList() {
const [products, setProducts] = useState(null);
if (!products) {
return (
<div className="skeleton-container">
{[...Array(10)].map((_, i) => (
<div key={i} className="product-skeleton">
<div className="skeleton-image"></div>
<div className="skeleton-text"></div>
<div className="skeleton-text short"></div>
</div>
))}
</div>
);
}
return <RealProductList products={products} />;
}
坑3:SSR服务器压力大
我们的解决方案:
- 缓存:给经常访问的页面加缓存
- 降级策略:流量太大时,自动降级为CSR
- 边缘渲染:把SSR放到CDN边缘节点
// 简单的缓存中间件
const ssrCache = new Map();
app.get('/product/:id', async (req, res) => {
const cacheKey = `product-${req.params.id}`;
// 检查缓存
if (ssrCache.has(cacheKey)) {
const { html, timestamp } = ssrCache.get(cacheKey);
// 缓存5分钟内有效
if (Date.now() - timestamp < 5 * 60 * 1000) {
return res.send(html);
}
}
// 重新渲染
const html = await renderProductPage(req.params.id);
// 更新缓存
ssrCache.set(cacheKey, {
html,
timestamp: Date.now()
});
res.send(html);
});
我的技术选型心法
经过这些年的实践,我总结了一个简单的决策框架:
function 选择渲染策略(项目需求) {
if (需求.需要SEO && 需求.内容动态) {
return 'SSR'; // 如新闻网站、电商详情页
}
if (需求.需要SEO && !需求.内容动态) {
return 'SSG'; // 如公司官网、博客
}
if (!需求.需要SEO && 需求.高度交互) {
return 'CSR'; // 如后台管理系统、Web应用
}
// 其他情况,考虑混合方案
return 'SSR+CSR混合';
}
但说实话,现在有了Next.js、Nuxt.js这些框架,我们不必从一开始就做艰难的选择。它们支持各种渲染模式,你甚至可以在同一个应用的不同页面使用不同的策略。
未来:React Server Components带来的思考
最近我在试验React Server Components,感觉它可能改变游戏规则。让我眼前一亮的是:
// Server Component - 只在服务端运行
async function ProductDetails({ productId }) {
// 可以直接访问数据库,不需要API层
const product = await db.products.findUnique({
where: { id: productId }
});
// 这个组件不会被打包到客户端bundle中
return (
<div>
<h1>{product.name}</h1>
<ProductReviews productId={productId} />
</div>
);
}
// 在客户端组件中使用
'use client';
function ProductPage({ productId }) {
return (
<div>
<Suspense fallback={<div>加载产品信息...</div>}>
{/* 这个组件在服务端渲染,零客户端bundle */}
<ProductDetails productId={productId} />
</Suspense>
{/* 这个是客户端交互组件 */}
<AddToCartButton productId={productId} />
</div>
);
}
这让我思考:也许未来的前端开发,不再需要纠结于“在哪里渲染”,而是根据组件的特性自然选择执行环境。
写在最后:技术选择是一种权衡
回顾这些年前端渲染的演变,我意识到没有完美的解决方案,只有适合当前场景的选择。
- 早期网站:都是SSR,因为那时候连JavaScript都不成熟
- SPA时代:CSR成为主流,追求桌面应用般的体验
- 现在:混合方案,根据需求选择最合适的工具
作为开发者,我们容易陷入“技术决定论”——认为某个技术一定优于另一个。但真实项目中,需要考虑的因素太多了:团队技能、项目规模、性能要求、SEO需求、维护成本...
所以我的建议是:
- 从简单开始:新项目可以从CSR开始,快速验证想法
- 按需优化:遇到具体问题(如SEO、首屏性能)时,再引入SSR
- 保持开放:新技术不断出现,保持学习但不要盲目跟风
最后分享一个让我释怀的观点:Web的本质是渐进增强。无论选择哪种渲染方式,都要确保基础内容能被快速访问,然后在此基础上增强体验。

浙公网安备 33010602011771号