客户端渲染(CSR)详解
使用 npm create vue@latest 或 npm create vite@latest 创建一个 Vue3 项目时,默认获得的便是典型的客户端渲染模式。在此模式下,服务器返回的 HTML 文件内容极为精简,核心结构仅为一个 与多个 标签。浏览器需要先完整下载所有 JavaScript 文件,再解析并执行代码,之后 Vue 才会在浏览器中动态构建 DOM 树,最终将页面内容呈现出来。

这种方案的优势较为显著:前后端实现完全解耦,开发体验直观流畅,页面切换无需刷新,交互感受接近原生应用;同时服务器负载极低,仅需托管静态文件即可。
然而其代价同样不容忽视。首屏加载时间成为明显短板——用户必须等待 JS 下载、解析并执行完毕才能看到页面内容;搜索引擎爬虫通常不会执行 JavaScript,导致 SEO 表现很不理想;此外还需额外处理 meta 标签(标题、描述等),增加了工作量。
预渲染技术实现方案
预渲染的核心思路是在构建阶段发力。它会启动一个无头浏览器(如 Puppeteer),依次访问指定的路由,让 Vue 代码真实运行一次,然后将渲染完成的完整 HTML 直接保存为静态文件。当用户请求这些路由时,服务器直接返回这份现成的 HTML,客户端无需再次执行 Vue 逻辑。
这种方式保留了客户端渲染的开发体验,同时有效解决了首屏白屏问题,并改善了 SEO 表现。部署也十分便捷,交给 Nginx 或 CDN 即可。对于页面数量不多、内容相对固定的站点而言,这是一个非常合适的选择。
但缺点也相当明确:如果路由数量达到成百上千,构建时间会呈指数级增长,带来巨大困扰。另外 HTML 在构建时已经固化,无法处理动态数据——例如用户评论、实时价格等场景,显得力不从心。更麻烦的是参数化路由,比如 /user/:id 这种形式,必须在构建时穷举所有可能的 ID,这在很多实际场景中几乎无法实现。
实践示例
常用的插件为 @seresweb/vite-plugin-seo-prerender。
vue3-mananger-prerender/vite.config.ts
import { fileURLToPath, URL } from "node:url";
import { defineConfig } from "vite";
import vue from "@vitejs/plugin-vue";
import vueJsx from "@vitejs/plugin-vue-jsx";
// import VueDevTools from 'vite-plugin-vue-devtools'
import seoPrerender from "@seresweb/vite-plugin-seo-prerender";
// import { createMpaPlugin } from "vite-plugin-virtual-mpa";
import AutoImport from "unplugin-auto-import/vite";
import Components from "unplugin-vue-components/vite";
import { ElementPlusResolver } from "unplugin-vue-components/resolvers";
// @ts-ignore
import prerenderFallback from "./plugin/vite-plugin-prerender-fallback.js";// 预渲染路由列表(从 router 中提取需要预渲染的静态路由)
// const prerenderRoutes = ["/", "/home", "/dashboard", "/about", "/404"];
//
export default defineConfig(({ command }) => {
// const isBuild = command === "build"; return {
plugins: [
vue(),
vueJsx(),
AutoImport({
resolvers: [ElementPlusResolver()],
// 推荐:生成类型声明文件,提升 TypeScript 支持
dts: "src/auto-imports.d.ts",
}),
Components({
resolvers: [ElementPlusResolver()],
// 推荐:生成组件类型声明文件,提升 TypeScript 支持
dts: "src/components.d.ts",
}),
// VueDevTools(), prerenderFallback({
distDir: "dist", // 可选,默认就是 'dist'
}),
seoPrerender({
routes: ["/yoy/dashboard"],
headless: "new",
executablePath: undefined,
}),
],
resolve: {
alias: {
"@": fileURLToPath(new URL("./src", import.meta.url)),
},
},
build: {
outDir: "dist",
target: "es2015",
minify: true,
},
};
});
此外还需要一个自定义插件来处理 fallback 逻辑:当导航栏路径变化时,先尝试请求对应目录下的 index.html 文件,若找不到则走默认回退。
vue3-mananger-prerender/plugin/vite-plugin-prerender-fallback.js
// vite-plugin-prerender-fallback.js
import fs from 'node:fs';
import path from 'node:path';/**
* Vite 插件:为预览服务器添加预渲染目录回退 + SPA fallback
* @param {Object} options
* @param {string} options.distDir - 构建输出目录,默认 'dist'
*/
export default function prerenderFallback(options = {}) {
const { distDir = 'dist' } = options;
const distPath = path.resolve(process.cwd(), distDir); return {
name: 'vite-plugin-prerender-fallback',
apply: 'serve', // 仅在预览/开发服务器时生效(预览模式也是 serve)
configurePreviewServer(server) {
server.middlewares.use((req, res, next) => {
// 1. 跳过根路径、带扩展名的文件请求、API 等
const url = req.url;
if (
url === '/' ||
path.extname(url) !== '' ||
url.startsWith('/@') ||
url.startsWith('/node_modules/')
) {
return next();
}
// 2. 构造预渲染 HTML 的完整路径:dist + url + /index.html
const htmlPath = path.join(distPath, url, 'index.html');
// 3. 同步检查文件是否存在
if (fs.existsSync(htmlPath)) {
// 存在预渲染文件,修改请求路径指向该文件
req.url = path.posix.join(url, 'index.html');
return next();
}
// 4. 不存在预渲染文件,最终会 fallback 到根 index.html
return next();
});
},
};
}
以上只是一个测试 demo,实际项目需要根据需求做更精细的调整。
静态站点生成(SSG)全面解析
SSG 本质上可视为预渲染的全面升级版。它不再局限于少数路由,而是在构建阶段将整个站点完整转换为静态 HTML。它支持动态路由(通过 getStaticPaths 枚举所有可能的参数),还能在构建时从 API 或本地文件拉取数据(getStaticProps)。生成的每个页面都是完全独立的、包含全部内容的静态 HTML 文件。
毫无疑问,首屏速度达到极致,搜索引擎优化效果完美——爬虫直接获取完整内容,无需任何执行。同时开发体验仍是熟悉的 Vue 组件模式,部署成本低,CDN 即可轻松搞定。
但代价体现在构建时间上:页面数量越多,构建时间越长,百万级的大型站点几乎难以承受。另外每次内容更新都需要全站重新构建,灵活性不足。如果需要处理用户个性化内容(如登录状态、推荐列表),它也无法直接实现,必须配合客户端的 JavaScript 二次请求数据才行。
服务端渲染(SSR)深度介绍
SSR 的逻辑是:当用户请求页面时,服务器动态运行 Vue 组件,在服务端生成完整的 HTML 字符串,并直接返回给浏览器。客户端拿到这份 HTML 后可以立即展示内容,随后通过“激活”(Hydration)让 Vue 组件恢复交互能力。由于每次请求都重新渲染,展示的数据始终保持最新状态。
SSR 带来的好处非常扎实:搜索引擎完美收录——任何爬虫看到的都是完整内容;首屏速度快,用户无需等待 JS 执行即可看到页面内容;同时天然支持实时数据——用户登录态、实时评论、个性化推荐等场景都能直接满足。
但其代价也相当显著。服务器压力实实在在——每次请求都需要消耗 CPU 去渲染组件。工程复杂度也大幅提升:需要处理 Node.js 运行环境问题、内存泄漏、缓存策略,还要时刻防范客户端与服务端状态不一致。首字节时间(TTFB)也会比直接返回静态文件高出一截。
