Vue3 服务端渲染实战:SSR 数据预取与流式传输的全栈协作
Vue3 服务端渲染实战SSR 数据预取与流式传输的全栈协作一、SPA 的 SEO 之殇当搜索引擎面对你的空白页面Vue3 的单页应用SPA构建出的 HTML 通常只有一个div idapp/div。搜索引擎的爬虫访问你的页面时看到的是一片空白。对于内容驱动型网站博客、电商、文档站这意味着你的内容对搜索引擎完全不可见。客户端渲染CSR还有一个更深层的问题首屏渲染依赖 JavaScript 的下载、解析、执行。在 3G 网络或低端设备上用户可能需要等待 5-8 秒才能看到内容。对于内容型产品每增加 1 秒加载时间跳出率提升约 10%。Vue3 的服务端渲染SSR同时在服务端生成完整 HTML 和在客户端进行水合Hydration解决了 SEO 和首屏性能两个问题。但 SSR 引入了新的工程挑战数据预取的时序、流式传输的协调、状态同步的一致性。sequenceDiagram participant Browser as 浏览器 participant SSR as Vue3 SSR 服务 participant API as 后端 API participant DB as 数据库 Browser-SSR: GET /article/123 SSR-API: fetchArticle(123) API-DB: SELECT article DB--API: 文章数据 API--SSR: {title, content, author} Note over SSR: 服务端渲染 HTML SSR--Browser: 完整 HTML (含内容) Note over Browser: 首屏立即可见 Browser-SSR: 加载 JS bundle SSR--Browser: app.js Note over Browser: Hydration 激活 Browser-API: 客户端数据预取 API--Browser: 最新数据如有变化 Note over Browser: 页面完全可交互本文将基于 Vue3 Vite SSR 构建一个完整的服务端渲染方案覆盖数据预取、流式传输和状态同步三个核心环节。二、SSR 数据预取的时序模型从asyncData到SuspenseVue3 SSR 的数据预取有两种模式同步等待阻塞渲染和异步并发Suspense。同步等待是最简单的模式在服务端执行所有数据请求等待全部完成后渲染 HTML一次性返回给客户端。优点是实现简单状态一致性好。缺点是需要等待最慢的请求——如果页面上有 3 个数据源分别耗时 100ms、200ms 和 3s用户需要等 3s 才能看到任何内容。Suspense 异步组件是 Vue3 提供的改进方案。将页面拆分为独立的数据请求块每个块包裹在Suspense中。服务端先渲染不需要数据的内容如导航栏、页脚数据依赖的组件在数据到达后逐个替换 fallback。配合renderToStream()可以实现真正的流式 HTML 输出。关键限制Suspense的流式渲染只在renderToStream()中生效。如果使用renderToString()Suspense 会退化为同步等待模式。三、Vue3 SSR 的完整工程实现服务端入口entry-server.tsimport { createSSRApp } from vue; import { renderToString } from vue/server-renderer; import { createRouter } from ./router; import App from ./App.vue; export async function render(url: string) { const app createSSRApp(App); const router createRouter(); await router.push(url); await router.isReady(); // 收集组件中声明的数据预取函数 const matchedComponents router.currentRoute.value.matched .flatMap(record Object.values(record.components)); const dataFetchPromises matchedComponents .filter((comp: any) comp.asyncData) .map((comp: any) comp.asyncData({ route: router.currentRoute.value, store: {}, // Pinia store })); // 并行执行所有数据预取 const fetchedData await Promise.all(dataFetchPromises); // 将预取数据注入到应用的上下文中 const context { fetchedData }; const html await renderToString(app, context); return { html, fetchedData }; }客户端入口entry-client.tsimport { createSSRApp } from vue; import { createRouter } from ./router; import App from ./App.vue; const app createSSRApp(App); const router createRouter(); // 从服务端注入的 __INITIAL_STATE__ 恢复数据 if (window.__INITIAL_STATE__) { // 恢复 Pinia store 状态 // store.$patch(window.__INITIAL_STATE__); } router.beforeResolve(async (to) { const components to.matched.flatMap(r Object.values(r.components)); const asyncDataFns components.filter((c: any) c.asyncData); // 客户端导航时也执行数据预取 await Promise.all(asyncDataFns.map((fn: any) fn({ route: to }))); }); router.isReady().then(() { app.mount(#app, true); // true 水合模式 });Vite SSR 配置// vite.config.ts import { defineConfig } from vite; import vue from vitejs/plugin-vue; export default defineConfig({ plugins: [vue()], build: { ssr: src/entry-server.ts, ssrManifest: true, }, ssr: { noExternal: [vue/server-renderer], }, });流式渲染使用renderToStreamimport { renderToStream } from vue/server-renderer; import { createSSRApp } from vue; async function streamRender(req, res) { const app createSSRApp(/* ... */); // 先发送 HTML 头部 res.write(!DOCTYPE htmlhtmlhead.../headbodydiv idapp); const stream renderToStream(app); // 管道传输到响应 stream.pipe(res, { end: false }); stream.on(end, () { res.end(/div/body/html); }); }组件中使用asyncDatascript setup langts interface ArticleData { title: string; content: string; } // asyncData 在服务端和客户端导航时都会被调用 // 此函数必须是导出的便于 SSR 入口收集 export async function asyncData({ route }: { route: any }) { const response await fetch(https://api.example.com/articles/${route.params.id}); if (!response.ok) { throw new Error(文章加载失败: ${response.status}); } return response.json(); } const props defineProps{ article?: ArticleData }(); /script四、SSR 的工程代价与决策边界服务端成本SSR 需要 Node.js 服务器增加了运维复杂度。每个请求都会消耗服务器 CPU 来渲染 Vue 组件。对于高并发内容站建议在 SSR 前面加 CDN 缓存如按 URL 缓存渲染结果 5 分钟。开发体验下降服务端没有window对象没有 DOM API。任何依赖浏览器 API 的代码都需要用if (import.meta.env.SSR)做条件判断。第三方库如果不支持 SSR会导致服务端渲染崩溃。水合不匹配如果服务端渲染的 HTML 和客户端水合后的 Virtual DOM 不一致Vue 会报 Hydration Mismatch 警告。常见原因服务端和客户端使用了不同的时区、随机数、或依赖于浏览器特性的计算。不适用场景强交互的管理后台Dashboard——不需要 SEOCSR 开发体验更好纯静态内容站——使用 SSGStatic Site Generation预渲染更优对首屏速度要求极高的移动端 H5——考虑 SSR Service Worker 的离线方案五、总结Vue3 SSR 的核心价值在于服务端渲染SEO 首屏和客户端水合交互的协同工作。落地路径先用renderToStringasyncData模式验证 SSR 流程的正确性然后评估是否需要流式输出——如果页面有 3 个以上独立的数据源引入renderToStreamSuspense可以显著改善 TTFBTime to First Byte最后配置 CDN 边缘缓存将 SSR 的服务器成本降到最低。SSR 不是万能药。在引入 SSR 之前先确认你的产品确实需要 SEO 和首屏速度——对于 80% 的 SaaS 产品这两点都不是刚需。少即是多。