渲染模式
演进:
csr (Client-Side Rendering)客户端渲染:
- 优点:部署简单、交互体验好,适合复杂交互应用
- 缺点:seo 不友好(返回空 html),解析 js 才能生产页面,首屏时间长白屏,状态前端接管变得复杂
SSR (Server-Side Rendering)服务端渲染:
把数据拉取拼接成 HTML 在服务端完成,浏览器重新渲染加载一遍 js,也就是水合(注册事件等),才有交互性
- 优点:返回完整 html ,故 seo 友好、首屏 FCP 变快
- 缺点:消耗服务器资源,页面能更快看到,但是 TTI 基本不变,页面看到但是不能交互,TTFB 时间长
ssg (Static-Site Generation)静态站点生成:
完全静态页面,构建时候生产硬编码 HTML
- 优点:相比 ssr,不需要获取数据再拼接成 HTML,故 TTFB、FCP 时间变短
- 缺点:也需要进行水合,内容变更需要重新构建,麻烦
如果你调研 SSR 只是为了优化为数不多的营销页面的 SEO (例如 /
、 /about
和 /contact
等),那么你可能需要 SSG 而不是 SSR。
isg/isr:增量静态生成 / 再生 Incremental Static Regeneration (ISR)
与 ssg 相比增加服务器运行时,会按照一定的策略刷新策略来重新生产页面
Progressive Hydration - 渐进水合:
传统的 ssr 需要加载完整的 js 页面才能具有交互性,导致 TTI 变晚,故可以通过代码分割,将某些组件抽取为异步组件(按照一定规则按需水合),降低主包体积
SSR with streaming - 流式 SSR:
相比传统 ssr,HTML 不是一次性服务端生成好返回,而是生成一部分就返回一部分,加快了 FFTB 和 FCP,在页面内容较长时效果较好
Selective Hydration - 选择性水合:
是渐进式水合 (Progressive Hydration) 和 流式 SSR (SSR with Streaming) 的升级版。主要通过选择性地跳过‘慢组件’,避免阻塞,来实现更快的 HTML 输出
- Suspend 包裹起来,准备好之后将结果替换插槽
慢组件通常指的是:需要异步获取数据、体积较大、或者是计算量比较复杂的组件
Islands Architecture - 岛屿架构(去 javascript):
岛屿架构的主要代表是 Astro。在服务端渲染后,在客户端侧没有客户端程序和水合的过程。而对于需要 JavaScript 增强,实现动态交互的组件,需要显式标记为岛屿
- 每个岛屿都是独立加载、局部水合。而 Progressive Hydration 是整棵树水合的分支,只不过延后了
- 岛屿可以框架无关
- 缓解 TTI,适合内容静态大于动态的场景
以 <NuxtIsland>
为例 无需任何客户端 JS ,在服务端完成水合好了发过来 减少 bundle 大小,在渲染岛屿组件时,岛屿组件的内容是静态的,因此客户端无需下载 JS。更改岛屿组件道具会触发岛屿组件的重新获取,从而再次重新渲染。
RFC 服务端组件 React RFC Server Components:
服务器组件类似于纯函数没有 hooks,没有状态
仅在浏览器运行的代码
在 SSR(Server-Side Rendering)项目中,处理仅在浏览器运行的代码是确保应用在服务器和客户端环境都能正常工作的关键。以下是具体的处理策略和代码实践:
环境 | 服务器(Node.js) | 浏览器 |
---|---|---|
API 支持 | 无 window 、 document 等 DOM API | 支持完整的 DOM 和浏览器 API |
生命周期 | 只执行初始化渲染逻辑 | 执行渲染 + 交互逻辑(事件、状态更新等) |
数据获取 | 直接访问数据库或后端 API | 通过 fetch / XMLHttpRequest 请求 |
全局变量 | process.env (Node 环境变量) | window 、 localStorage 等 |
1. 环境判断与代码隔离
通过运行时环境判断,对浏览器专用代码进行隔离。
示例 1:直接判断全局变量
// 通用方式:判断是否存在 window 或 document
if (typeof window !== 'undefined') {
// 浏览器环境执行代码
window.addEventListener('resize', handleResize);
}
2
3
4
5
示例 2:使用构建时环境变量(推荐)
利用 Vite/Rollup 的 环境变量注入,动态区分环境:
// vite.config.js
export default {
define: {
__IS_BROWSER__: !process.env.SSR // 标记浏览器环境
}
}
// 业务代码中
if (__IS_BROWSER__) {
// 仅在浏览器执行的逻辑(如访问 localStorage)
const token = localStorage.getItem('token');
}
2
3
4
5
6
7
8
9
10
11
12
2. 生命周期钩子延迟执行
在某些前端框架(如 React、Vue)中,浏览器专用代码应在客户端生命周期阶段执行。
React 示例:使用 useEffect
import { useEffect } from 'react';
function MyComponent() {
useEffect(() => {
// 仅在浏览器执行
const button = document.getElementById('my-button');
button.addEventListener('click', handleClick);
return () => button.removeEventListener('click', handleClick);
}, []);
return <button id="my-button">Click me</button>;
}
2
3
4
5
6
7
8
9
10
11
12
Vue 示例:使用 mounted 钩子
<script>
export default {
mounted() { // 仅浏览器阶段执行
window.analytics.track('pageView');
}
}
</script>
2
3
4
5
6
7
3. 动态导入(Dynamic Import)
将浏览器专用代码包装成异步模块,在客户端按需加载(SSR 不执行)。
// 使用动态导入加载浏览器专用库(如图表库)
if (typeof window !== 'undefined') {
import('heavy-browser-lib').then((lib) => {
lib.renderChart();
});
}
2
3
4
5
6
4. 第三方库替换 / 占位
针对某些仅在浏览器环境生效的库(如 swiper
、 mapbox-gl
),在 SSR 阶段需要做兼容处理。
<!-- Vue 组件示例 -->
<template>
<client-only>
<!-- 仅在浏览器渲染 -->
<swiper :options="swiperOptions"/>
</client-only>
</template>
2
3
4
5
6
7
5. 构建配置分离
在 Vite 或 Webpack 中,为 SSR 和客户端分别配置不同的入口和依赖。
Vite 的 SSR 构建配置
// vite.config.js
export default {
build: {
ssr: true, // 标记为 SSR 构建
rollupOptions: {
input: 'src/entry-server.js' // 服务器专用入口
}
}
}
2
3
4
5
6
7
8
9
三、常见场景与代码处理
1. 访问浏览器全局对象
如操作 localStorage
、 sessionStorage
、 window.location
等:
// 安全封装一个浏览器存储工具
export const getStorageItem = (key) => {
if (typeof window === 'undefined') return null;
return localStorage.getItem(key);
};
2
3
4
5
2. 使用浏览器 API(如 IntersectionObserver)
// 封装一个安全的 IntersectionObserver 组件
const ObserverComponent = () => {
useEffect(() => {
const observer = new IntersectionObserver(callback, options);
observer.observe(element);
return () => observer.disconnect();
}, []);
return <div ref={elementRef}>...</div>;
};
2
3
4
5
6
7
8
9
10
3. 避免服务端渲染时请求客户端数据接口
将数据获取逻辑统一为 SSR 阶段预取 + 客户端水合(Hydrate):
// 服务端渲染时从数据库获取,客户端从全局状态读取
async function fetchData() {
if (typeof window === 'undefined') {
// 服务端直接请求数据库
return fetchFromDatabase();
} else {
// 客户端从全局状态或注水数据读取
return window.__INITIAL_STATE__.data;
}
}
2
3
4
5
6
7
8
9
10