跳到主要内容

Vue 服务端渲染

问题

什么是 Vue SSR(服务端渲染)?Vue 中如何实现同构应用?Nuxt.js 的核心架构是什么?

面试速答版

什么是 Vue SSR(服务端渲染)? SSR 是在 Node.js 服务端把 Vue 组件渲染成 HTML 字符串,浏览器拿到后直接展示,再由客户端 JS「激活」(Hydration)成可交互的应用。

  • 解决两个核心问题:首屏速度(不用等 JS 下载执行就能看到内容)和 SEO(爬虫拿到的是完整 HTML)。
  • 关键 API:服务端用 createSSRApp + renderToString(来自 @vue/server-renderer),客户端用 createSSRApp + app.mount
  • 与 CSR/SSG/ISR 的区别:CSR 是浏览器渲染;SSG 是构建时预渲染成静态 HTML;ISR 是 SSG + 按需重新生成。

Vue 中如何实现同构应用? 同构 = 同一套代码同时跑在 Node 和浏览器,几个关键点:

  • 每个请求创建新实例:服务端是长进程,多个请求共享实例会串数据,所以必须 createSSRApp 而不是单例。
  • 路由/store 也要每请求新建:用工厂函数 createRouter / createPinia,避免跨请求污染。
  • 数据预取:在路由匹配后、渲染前就把组件依赖的数据请求完,注入到 store/props 里;同时把 state 序列化到 HTML,客户端激活时复用,避免重复请求。
  • 避免使用 window / document:服务端没有 DOM,要用 import.meta.env.SSRonMounted 守住浏览器才执行的代码。
  • 激活一致性:客户端首次渲染必须产出和服务端一模一样的 DOM,否则会报 hydration mismatch。

Nuxt.js 的核心架构是什么? Nuxt 是 Vue 官方的 SSR 框架,把上面的「脏活」全部封装好:

  • 基于文件的路由pages/ 目录自动生成路由,支持动态路由、嵌套路由、布局。
  • Nitro 引擎:底层 server,支持多种部署目标(Node、Vercel、Cloudflare Workers、Deno 等),同一份代码部署到不同平台。
  • 多种渲染模式:通过 routeRules 灵活配置每个页面是 SSR / SSG / ISR / SPA。
  • 数据获取:内置 useFetch / useAsyncData,自动处理服务端预取 + 状态序列化。
  • 自动导入:组件、composable、ref/computed 等都自动导入。
  • 模块生态@nuxt/image@nuxtjs/i18n@pinia/nuxt 等模块开箱即用。

答案

SSR(Server-Side Rendering)是指在服务端将 Vue 组件渲染为 HTML 字符串,发送给浏览器后再由客户端"激活"(Hydration)为可交互的应用。Vue 的 SSR 方案基于同构(Isomorphic / Universal)理念——同一套 Vue 代码同时运行在 Node.js 服务端和浏览器客户端。

为什么需要 SSR
  • 首屏速度:用户直接看到渲染好的 HTML,无需等待 JS 下载和执行
  • SEO 友好:搜索引擎爬虫可以直接抓取完整的 HTML 内容
  • 社交分享:OG 标签中的动态内容可以被正确抓取

CSR vs SSR vs SSG vs ISR 对比

维度CSRSSRSSGISR
渲染位置浏览器服务端(每次请求)构建时构建时 + 按需重新生成
首屏速度慢(白屏)最快(CDN 直出)快(CDN 缓存)
SEO
服务器压力高(每次请求渲染)无(纯静态)低(缓存 + 按需)
数据实时性实时实时构建时快照可配过期时间
Vue 实现createAppcreateSSRAppNuxt generateNuxt routeRules

更多渲染策略对比请参考 SSR 与 SSG


Vue SSR 核心 API

1. 服务端:createSSRApp + renderToString

server/entry-server.ts
import { createSSRApp } from 'vue'
import { renderToString } from '@vue/server-renderer'
import App from './App.vue'

export async function render() {
// 每个请求创建新的应用实例,避免跨请求状态污染
const app = createSSRApp(App)

// 渲染为 HTML 字符串
const html = await renderToString(app)
return html
}
每个请求必须创建新实例

服务端代码运行在一个长期存活的 Node.js 进程中。如果多个请求共享同一个 Vue 实例,会导致跨请求状态泄漏——用户 A 的数据被用户 B 看到。

2. 客户端:Hydration

client/entry-client.ts
import { createSSRApp } from 'vue'
import App from './App.vue'

const app = createSSRApp(App)

// 客户端激活:复用服务端渲染的 DOM,绑定事件监听器
app.mount('#app')

Hydration 的本质是:客户端 Vue 不会重新创建 DOM,而是复用服务端生成的 HTML 节点,只附加事件监听器和响应式状态。

3. 流式渲染:renderToNodeStream

server/stream-render.ts
import { renderToNodeStream } from '@vue/server-renderer'
import { createSSRApp } from 'vue'
import App from './App.vue'

export function renderStream(res: NodeJS.WritableStream) {
const app = createSSRApp(App)
// 流式输出:边渲染边发送,减少 TTFB
const stream = renderToNodeStream(app)
stream.pipe(res)
}
流式渲染的优势

renderToString 需要等整个组件树渲染完才发送 HTML;renderToNodeStream 则可以边渲染边发送,大幅减少首字节时间(TTFB),特别适合页面内容较多的场景。


SSR 渲染流程


同构注意事项

1. 服务端无 DOM 环境

服务端没有 windowdocumentnavigator 等浏览器 API。在组件代码中直接访问这些 API 会导致运行时错误。

composables/useWindowSize.ts
import { ref, onMounted } from 'vue'

export function useWindowSize() {
const width = ref(0)
const height = ref(0)

// onMounted 只在客户端执行,安全地访问 window
onMounted(() => {
width.value = window.innerWidth
height.value = window.innerHeight
})

return { width, height }
}

2. 生命周期差异

服务端只会执行有限的生命周期钩子:

钩子服务端客户端
setup()执行执行
onServerPrefetch执行不执行
onBeforeMount不执行执行
onMounted不执行执行
onBeforeUpdate不执行执行
onUpdated不执行执行
onBeforeUnmount不执行执行
onUnmounted不执行执行

详细生命周期说明参考 Vue 生命周期

副作用需放在 onMounted 中

setIntervaladdEventListener、操作 DOM 等副作用必须放在 onMounted 内。在 setup 顶层执行这些操作会在服务端报错,也可能导致内存泄漏。

3. 全局状态污染

server/entry-server.ts
// ❌ 错误:所有请求共享同一个 store,状态会泄漏
// const store = createPinia()

export function createApp() {
const app = createSSRApp(App)
// ✅ 正确:每个请求创建新的 Pinia 实例
const pinia = createPinia()
app.use(pinia)
return { app, pinia }
}

4. 第三方库兼容性

许多前端库(如 echartsswiper)依赖浏览器 API,需要特殊处理:

components/ChartWrapper.vue
<script setup lang="ts">
import { onMounted, shallowRef } from 'vue'

// 延迟导入,只在客户端加载 echarts
const chartInstance = shallowRef(null)

onMounted(async () => {
const echarts = await import('echarts')
chartInstance.value = echarts.init(document.getElementById('chart'))
})
</script>

数据预取(Data Fetching)

onServerPrefetch(Composition API)

onServerPrefetch 是 Vue 3 专为 SSR 设计的钩子,在服务端渲染前异步获取数据:

pages/UserProfile.vue
<script setup lang="ts">
import { ref, onServerPrefetch, onMounted } from 'vue'

const user = ref(null)

async function fetchUser() {
const res = await fetch('/api/user/1')
user.value = await res.json()
}

// 服务端:渲染前获取数据
onServerPrefetch(async () => {
await fetchUser()
})

// 客户端:如果服务端已获取数据则跳过
onMounted(() => {
if (!user.value) {
fetchUser()
}
})
</script>

状态序列化与恢复

服务端获取的数据需要序列化到 HTML,客户端 Hydration 时恢复,避免重复请求:

server/render.ts
import { renderToString } from '@vue/server-renderer'

async function render(req) {
const { app, pinia } = createApp()

const html = await renderToString(app)
// 将 Pinia 状态序列化,注入到 HTML
const state = JSON.stringify(pinia.state.value)

return `
<div id="app">${html}</div>
<script>window.__INITIAL_STATE__ = ${state}</script>
`
}
client/entry-client.ts
const { app, pinia } = createApp()

// 客户端恢复服务端状态
if (window.__INITIAL_STATE__) {
pinia.state.value = window.__INITIAL_STATE__
}

app.mount('#app')

Nuxt.js SSR 实践

Nuxt 3 是 Vue 生态最主流的 SSR 框架,基于 Nitro 服务引擎,提供开箱即用的 SSR、SSG、ISR 支持。

Nuxt 3 架构

渲染模式对比

Nuxt 3 通过 nuxt.config.ts 配置不同的渲染模式:

模式配置说明
Universal(SSR)ssr: true(默认)服务端渲染 + 客户端 Hydration
SPAssr: false纯客户端渲染
HybridrouteRules 按路由配置不同路由使用不同渲染策略
SSGnpx nuxi generate构建时预渲染所有页面
nuxt.config.ts
export default defineNuxtConfig({
// Hybrid 渲染:按路由配置不同策略
routeRules: {
'/': { prerender: true }, // SSG:构建时预渲染
'/blog/**': { isr: 3600 }, // ISR:缓存 1 小时后重新生成
'/admin/**': { ssr: false }, // SPA:纯客户端渲染
'/api/**': { cors: true }, // API 路由配置
},
})

useFetch vs useAsyncData vs $fetch

APISSR 数据预取自动去重响应式适用场景
useFetch页面/组件数据获取(推荐)
useAsyncData自定义数据源(非 HTTP)
$fetch事件处理函数、客户端请求
pages/posts/[id].vue
<script setup lang="ts">
const route = useRoute()

// useFetch:SSR + 客户端导航均自动获取数据
const { data: post, pending, error } = await useFetch(
`/api/posts/${route.params.id}`
)
</script>

<template>
<div v-if="pending">加载中...</div>
<div v-else-if="error">加载失败</div>
<article v-else>
<h1>{{ post.title }}</h1>
<div v-html="post.content" />
</article>
</template>
composables/useCustomData.ts
// useAsyncData:自定义数据源
export function usePostWithTransform(id: string) {
return useAsyncData(`post-${id}`, async () => {
const raw = await $fetch(`/api/posts/${id}`)
// 可以做数据转换
return { ...raw, readTime: Math.ceil(raw.content.length / 500) }
})
}

Server API 路由

Nuxt 3 内置 server/api/ 目录,基于 Nitro 提供全栈能力:

server/api/posts/[id].get.ts
export default defineEventHandler(async (event) => {
const id = getRouterParam(event, 'id')

// 直接访问数据库,此代码只在服务端运行
const post = await db.query('SELECT * FROM posts WHERE id = ?', [id])

if (!post) {
throw createError({ statusCode: 404, message: 'Post not found' })
}

return post
})

Nuxt 中间件与路由守卫

middleware/auth.ts
export default defineNuxtRouteMiddleware((to, from) => {
const { loggedIn } = useUserSession()

// 服务端和客户端都会执行
if (!loggedIn.value && to.path !== '/login') {
return navigateTo('/login')
}
})

SSR 性能优化

1. 组件级缓存

对于不依赖用户状态的组件(如导航栏、页脚),可以缓存渲染结果:

nuxt.config.ts
export default defineNuxtConfig({
// Nuxt 3 通过 routeRules 实现页面级缓存
routeRules: {
'/blog/**': { swr: 3600 }, // SWR:先返回缓存,后台重新验证
},
})

2. 流式渲染

Vue 3 + Nuxt 3 支持流式 SSR,将 HTML 分块发送:

3. 代码分割与异步组件

pages/dashboard.vue
<script setup lang="ts">
// 异步组件:只在需要时加载,减少初始 bundle
const HeavyChart = defineAsyncComponent(() =>
import('~/components/HeavyChart.vue')
)
</script>

<template>
<Suspense>
<HeavyChart />
<template #fallback>
<div>图表加载中...</div>
</template>
</Suspense>
</template>

4. Edge Rendering

Nuxt 3 + Nitro 支持部署到边缘运行时,将 SSR 逻辑下沉到 CDN 节点:

平台配置优势
Cloudflare Workersnitro: { preset: 'cloudflare' }全球边缘节点,冷启动极快
Vercel Edgenitro: { preset: 'vercel-edge' }与 Vercel 平台深度集成
Deno Deploynitro: { preset: 'deno-deploy' }Deno 原生运行时

SSR 常见问题

Hydration Mismatch

当服务端渲染的 HTML 与客户端 Hydration 时生成的虚拟 DOM 不一致时,会触发 Hydration Mismatch 警告。

常见原因

  1. 时间/随机数:服务端和客户端的 Date.now()Math.random() 结果不同
  2. 浏览器特有 API:在 setup 中使用 window.innerWidth 导致服务端为 undefined
  3. HTML 规范差异:浏览器会自动修正不规范的 HTML(如 <p> 嵌套 <div>
composables/useClientOnly.ts
// 解决方案:使用 ref 延迟客户端特有内容
import { ref, onMounted } from 'vue'

export function useClientOnly() {
const isMounted = ref(false)
onMounted(() => {
isMounted.value = true
})
return { isMounted }
}

在 Nuxt 中可以直接使用 <ClientOnly> 组件:

<!-- 只在客户端渲染 -->
<ClientOnly>
<ThirdPartyWidget />
<template #fallback>
<div>加载中...</div>
</template>
</ClientOnly>

window is not defined

// Nuxt 中判断运行环境
if (import.meta.client) {
// 客户端代码
window.addEventListener('resize', handleResize)
}

// 或使用动态导入
const lib = import.meta.client ? await import('browser-only-lib') : null

内存泄漏

SSR 应用运行在长期存活的 Node.js 进程中,内存泄漏尤其需要注意:

SSR 内存泄漏陷阱
  • 模块级变量作为全局单例会在所有请求间共享
  • 未清理的事件监听器不会被自动回收
  • 大对象缓存没有 TTL 或 LRU 淘汰机制

Vue SSR vs React SSR 简要对比

维度Vue SSRReact SSR
核心 APIcreateSSRApp + renderToStringrenderToPipeableStream
主流框架Nuxt 3Next.js
Hydration自动对比 + 修复React 18 Selective Hydration
流式渲染renderToNodeStreamrenderToPipeableStream(React 18)
服务端组件不支持(Vapor Mode 探索中)React Server Components
数据预取onServerPrefetch / useFetchServer Components / fetch
状态管理Pinia SSR 集成无内置方案(RSC 自带)

更多 React SSR 内容参考 Next.js 核心知识,Vue Composition API 与 Options API 对比参考 Composition API vs Options API


常见面试问题

Q1: 什么是 SSR?为什么 Vue 需要 SSR?

答案

SSR 是在服务端将 Vue 组件渲染为 HTML 字符串,发送给浏览器后再 Hydration 为可交互应用。主要解决两个问题:

  1. 首屏性能:CSR 需要等待 JS 下载和执行才能显示内容,SSR 直接返回 HTML,用户更快看到页面
  2. SEO:搜索引擎爬虫可能无法执行 JavaScript,SSR 返回完整 HTML 确保内容可被索引

Q2: Vue SSR 中为什么每个请求都要创建新的应用实例?

答案

Node.js 服务端是一个长期运行的进程。如果所有请求共用同一个 Vue 实例和 Pinia store,用户 A 修改的状态会被用户 B 看到,造成跨请求状态污染。因此必须在每个请求中调用 createSSRApp()createPinia() 创建全新实例。

Q3: 什么是 Hydration?Hydration Mismatch 怎么解决?

答案

Hydration 是客户端 Vue 复用服务端渲染的 DOM 节点、附加事件监听器和响应式状态的过程。当服务端 HTML 与客户端虚拟 DOM 不一致时,会产生 Hydration Mismatch。

解决方法:

  • 避免在 setup 顶层使用 Date.now()Math.random() 等不确定值
  • 使用 <ClientOnly> 包裹浏览器特有内容
  • 确保 HTML 结构规范(不在 <p> 内嵌套 <div>

Q4: Vue SSR 中哪些生命周期钩子会在服务端执行?

答案

服务端只执行 setup()(同步部分)和 onServerPrefetchonMountedonUpdatedonBeforeUnmount 等钩子只在客户端执行。因此需要操作 DOM 或注册浏览器事件的逻辑必须放在 onMounted 中。详情参考 Vue 生命周期

Q5: onServerPrefetch 的作用是什么?

答案

onServerPrefetch 是 Vue 3 专为 SSR 设计的钩子,在服务端渲染执行异步操作(如数据获取)。Vue 会等待其中的 Promise resolve 后才开始渲染组件。这确保服务端输出的 HTML 包含完整的数据内容。

Q6: Nuxt 3 中 useFetch$fetch 有什么区别?

答案

  • useFetch:在 SSR 时自动在服务端执行数据获取,结果序列化到 HTML,客户端不会重复请求。返回响应式的 datapendingerror
  • $fetch:基于 ofetch 的底层请求工具,不参与 SSR 数据预取流程,每次调用都会发起请求。适用于事件处理函数(如按钮点击后请求)

Q7: 如何处理只在浏览器端运行的第三方库?

答案

三种方式:

  1. <ClientOnly> 组件:包裹使用该库的组件
  2. 动态导入:在 onMountedawait import('library')
  3. 环境判断if (import.meta.client) { ... }(Nuxt 3)

Q8: SSR 应用如何避免内存泄漏?

答案

  • 每个请求创建新实例:避免模块级单例
  • 避免模块级可变状态:如 const cache = new Map() 会在所有请求间共享且持续增长
  • 使用带 TTL 的缓存:如 lru-cache,避免缓存无限增长
  • 清理副作用setInterval 等需要在组件卸载时清理

Q9: Nuxt 3 的渲染模式有哪些?如何选择?

答案

场景推荐模式
内容站(博客、文档)SSG / ISR
电商首页、SEO 页SSR / ISR
管理后台SPA
混合需求Hybrid(routeRules 按路由配置)

Nuxt 3 支持通过 routeRules 为不同路由配置不同的渲染策略,实现 Hybrid 渲染。

Q10: 什么是流式渲染?有什么优势?

答案

流式渲染(Streaming SSR)是指服务端不等整个页面渲染完成,而是边渲染边发送 HTML 片段。用户能更快看到首屏内容(降低 TTFB)。Vue 3 通过 renderToNodeStream / renderToWebStream 支持流式渲染,Nuxt 3 默认启用。

Q11: SSR 和 SSG 怎么选?

答案

  • SSG:内容不频繁变化的页面(文档、博客),构建时生成静态 HTML,性能最优
  • SSR:内容实时变化或需要个性化的页面(用户主页、搜索结果),每次请求服务端渲染
  • ISR:兼顾两者,构建时生成静态页面,设定过期时间后自动重新生成

更多对比参考 SSR 与 SSGVue 性能优化

Q12: Nuxt 3 的 Nitro 引擎有什么特点?

答案

Nitro 是 Nuxt 3 的服务端引擎,核心特点:

  • 跨平台部署:同一套代码可以部署到 Node.js、Cloudflare Workers、Vercel Edge、Deno 等
  • 内置 API 路由server/api/ 目录自动注册为 API 端点
  • 自动代码分割:服务端代码按路由分割,减少冷启动时间
  • 文件系统路由server/api/server/middleware/server/plugins/ 自动扫描

Q13: Vue SSR 与 React SSR(Next.js)的主要区别是什么?

答案

最大的区别在于 Server Components。React 18 引入了 RSC(React Server Components),组件可以声明只在服务端运行,不发送 JS 到客户端。Vue 目前不支持 Server Components(Vapor Mode 尚在探索中)。此外,Nuxt 使用 useFetch / useAsyncData 进行数据预取,而 Next.js App Router 可以直接在 Server Component 中 await fetch()


相关链接