Appearance
7.3 SSR 之 renderToString:服务器端如何将 vnode 渲染成 HTML 字符串?
服务器端渲染(SSR)是现代 Web 应用的重要技术,它能够提升首屏加载速度、改善 SEO 效果,并提供更好的用户体验。Vue 3 的 SSR 实现位于 @vue/server-renderer 包中,本节将深入分析其核心机制,揭示 Vue 如何在服务器端将虚拟 DOM 转换为 HTML 字符串。
SSR 架构概览
核心文件结构
Vue 3 的服务端渲染主要由以下核心文件组成:
server-renderer/src/
├── renderToString.ts # 字符串渲染入口
├── renderToStream.ts # 流式渲染实现
├── render.ts # 核心渲染逻辑
├── internal.ts # 内部辅助函数
└── helpers/ # 渲染辅助函数
├── ssrCompile.ts # 服务端模板编译
├── ssrRenderComponent.ts # 组件渲染
├── ssrRenderAttrs.ts # 属性渲染
├── ssrRenderSuspense.ts # Suspense 处理
└── ...SSR 上下文
typescript
export type SSRContext = {
[key: string]: any
teleports?: Record<string, string> // Teleport 内容映射
/**
* @internal
*/
__teleportBuffers?: Record<string, SSRBuffer> // Teleport 缓冲区
/**
* @internal
*/
__watcherHandles?: (() => void)[] // 监听器清理函数
}renderToString 核心流程
渲染入口
renderToString 是 SSR 的主要入口函数:
typescript
export async function renderToString(
input: App | VNode,
context: SSRContext = {},
): Promise<string> {
if (isVNode(input)) {
// 原始 vnode,包装成应用
return renderToString(createApp({ render: () => input }), context)
}
// 渲染应用
const vnode = createVNode(input._component, input._props)
vnode.appContext = input._context
// 提供 SSR 上下文给组件树
input.provide(ssrContextKey, context)
// 渲染组件 VNode
const buffer = await renderComponentVNode(vnode)
// 展开缓冲区为字符串
const result = await unrollBuffer(buffer as SSRBuffer)
// 解析 Teleport 内容
await resolveTeleports(context)
// 清理监听器
if (context.__watcherHandles) {
for (const unwatch of context.__watcherHandles) {
unwatch()
}
}
return result
}缓冲区机制
SSR 使用缓冲区(Buffer)机制来处理异步渲染:
typescript
export type SSRBuffer = SSRBufferItem[] & { hasAsync?: boolean }
export type SSRBufferItem = string | SSRBuffer | Promise<SSRBuffer>
export type PushFn = (item: SSRBufferItem) => void
// 创建缓冲区
export function createBuffer() {
let appendable = false
const buffer: SSRBuffer = []
return {
getBuffer(): SSRBuffer {
return buffer
},
push(item: SSRBufferItem): void {
const isStringItem = isString(item)
// 字符串合并优化
if (appendable && isStringItem) {
buffer[buffer.length - 1] += item as string
return
}
buffer.push(item)
appendable = isStringItem
// 标记异步状态
if (isPromise(item) || (isArray(item) && item.hasAsync)) {
buffer.hasAsync = true
}
},
}
}缓冲区展开
缓冲区展开是将嵌套的异步缓冲区转换为最终 HTML 字符串的过程:
typescript
function nestedUnrollBuffer(
buffer: SSRBuffer,
parentRet: string,
startIndex: number,
): Promise<string> | string {
if (!buffer.hasAsync) {
// 同步缓冲区,直接展开
return parentRet + unrollBufferSync(buffer)
}
let ret = parentRet
for (let i = startIndex; i < buffer.length; i += 1) {
const item = buffer[i]
if (isString(item)) {
ret += item
continue
}
if (isPromise(item)) {
// 等待 Promise 解析
return item.then(nestedItem => {
buffer[i] = nestedItem
return nestedUnrollBuffer(buffer, ret, i)
})
}
// 递归处理嵌套缓冲区
const result = nestedUnrollBuffer(item, ret, 0)
if (isPromise(result)) {
return result.then(nestedItem => {
buffer[i] = nestedItem
return nestedUnrollBuffer(buffer, '', i)
})
}
ret = result
}
return ret
}
export function unrollBuffer(buffer: SSRBuffer): Promise<string> | string {
return nestedUnrollBuffer(buffer, '', 0)
}组件渲染机制
组件实例化
服务端组件渲染的核心是 renderComponentVNode 函数:
typescript
export function renderComponentVNode(
vnode: VNode,
parentComponent: ComponentInternalInstance | null = null,
slotScopeId?: string,
): SSRBuffer | Promise<SSRBuffer> {
// 创建组件实例
const instance = (vnode.component = createComponentInstance(
vnode,
parentComponent,
null,
))
if (__DEV__) pushWarningContext(vnode)
// 设置组件(SSR 模式)
const res = setupComponent(instance, true /* isSSR */)
if (__DEV__) popWarningContext()
const hasAsyncSetup = isPromise(res)
let prefetches = instance.sp /* LifecycleHooks.SERVER_PREFETCH */
// 处理异步 setup 和 serverPrefetch
if (hasAsyncSetup || prefetches) {
const p: Promise<unknown> = Promise.resolve(res as Promise<void>)
.then(() => {
if (hasAsyncSetup) prefetches = instance.sp
if (prefetches) {
return Promise.all(
prefetches.map(prefetch => prefetch.call(instance.proxy)),
)
}
})
.catch(NOOP)
return p.then(() => renderComponentSubTree(instance, slotScopeId))
} else {
return renderComponentSubTree(instance, slotScopeId)
}
}组件子树渲染
typescript
function renderComponentSubTree(
instance: ComponentInternalInstance,
slotScopeId?: string,
): SSRBuffer | Promise<SSRBuffer> {
if (__DEV__) pushWarningContext(instance.vnode)
const comp = instance.type as Component
const { getBuffer, push } = createBuffer()
if (isFunction(comp)) {
// 函数式组件
let root = renderComponentRoot(instance)
// 处理作用域 ID 透传
if (!(comp as FunctionalComponent).props) {
for (const key in instance.attrs) {
if (key.startsWith(`data-v-`)) {
;(root.props || (root.props = {}))[key] = ``
}
}
}
renderVNode(push, (instance.subTree = root), instance, slotScopeId)
} else {
// 普通组件
// 编译模板(如果需要)
if (
(!instance.render || instance.render === NOOP) &&
!instance.ssrRender &&
!comp.ssrRender &&
isString(comp.template)
) {
comp.ssrRender = ssrCompile(comp.template, instance)
}
const ssrRender = instance.ssrRender || comp.ssrRender
if (ssrRender) {
// 优化的 SSR 渲染
let attrs = instance.inheritAttrs !== false ? instance.attrs : undefined
let hasCloned = false
// 收集作用域 ID
let cur = instance
while (true) {
const scopeId = cur.vnode.scopeId
if (scopeId) {
if (!hasCloned) {
attrs = { ...attrs }
hasCloned = true
}
attrs![scopeId] = ''
}
const parent = cur.parent
if (parent && parent.subTree && parent.subTree === cur.vnode) {
cur = parent
} else {
break
}
}
// 处理插槽作用域 ID
if (slotScopeId) {
if (!hasCloned) attrs = { ...attrs }
const slotScopeIdList = slotScopeId.trim().split(' ')
for (let i = 0; i < slotScopeIdList.length; i++) {
attrs![slotScopeIdList[i]] = ''
}
}
// 执行 SSR 渲染函数
const prev = setCurrentRenderingInstance(instance)
try {
ssrRender(
instance.proxy,
push,
instance,
attrs,
// 编译器优化的绑定
instance.props,
instance.setupState,
instance.data,
instance.ctx,
)
} finally {
setCurrentRenderingInstance(prev)
}
} else if (instance.render && instance.render !== NOOP) {
// 使用 render 函数
renderVNode(
push,
(instance.subTree = renderComponentRoot(instance)),
instance,
slotScopeId,
)
} else {
// 缺少模板或渲染函数
const componentName = comp.name || comp.__file || `<Anonymous>`
warn(`Component ${componentName} is missing template or render function.`)
push(`<!---->`)
}
}
if (__DEV__) popWarningContext()
return getBuffer()
}VNode 渲染逻辑
核心渲染函数
renderVNode 是处理各种类型 VNode 的核心函数:
typescript
export function renderVNode(
push: PushFn,
vnode: VNode,
parentComponent: ComponentInternalInstance,
slotScopeId?: string,
): void {
const { type, shapeFlag, children, dirs, props } = vnode
// 应用 SSR 指令
if (dirs) {
vnode.props = applySSRDirectives(vnode, props, dirs)
}
switch (type) {
case Text:
push(escapeHtml(children as string))
break
case Comment:
push(
children
? `<!--${escapeHtmlComment(children as string)}-->`
: `<!---->`,
)
break
case Static:
push(children as string)
break
case Fragment:
if (vnode.slotScopeIds) {
slotScopeId =
(slotScopeId ? slotScopeId + ' ' : '') + vnode.slotScopeIds.join(' ')
}
push(`<!--[-->`) // 开始标记
renderVNodeChildren(
push,
children as VNodeArrayChildren,
parentComponent,
slotScopeId,
)
push(`<!--]-->`) // 结束标记
break
default:
if (shapeFlag & ShapeFlags.ELEMENT) {
renderElementVNode(push, vnode, parentComponent, slotScopeId)
} else if (shapeFlag & ShapeFlags.COMPONENT) {
push(renderComponentVNode(vnode, parentComponent, slotScopeId))
} else if (shapeFlag & ShapeFlags.TELEPORT) {
renderTeleportVNode(push, vnode, parentComponent, slotScopeId)
} else if (shapeFlag & ShapeFlags.SUSPENSE) {
renderVNode(push, vnode.ssContent!, parentComponent, slotScopeId)
} else {
warn(
'[@vue/server-renderer] Invalid VNode type:',
type,
`(${typeof type})`,
)
}
}
}元素渲染
typescript
function renderElementVNode(
push: PushFn,
vnode: VNode,
parentComponent: ComponentInternalInstance,
slotScopeId?: string,
) {
const tag = vnode.type as string
let { props, children, shapeFlag, scopeId } = vnode
let openTag = `<${tag}`
// 渲染属性
if (props) {
openTag += ssrRenderAttrs(props, tag)
}
// 添加作用域 ID
if (scopeId) {
openTag += ` ${scopeId}`
}
// 继承父组件链的作用域 ID
let curParent: ComponentInternalInstance | null = parentComponent
let curVnode = vnode
while (curParent && curVnode === curParent.subTree) {
curVnode = curParent.vnode
if (curVnode.scopeId) {
openTag += ` ${curVnode.scopeId}`
}
curParent = curParent.parent
}
if (slotScopeId) {
openTag += ` ${slotScopeId}`
}
push(openTag + `>`)
if (!isVoidTag(tag)) {
let hasChildrenOverride = false
// 处理特殊属性
if (props) {
if (props.innerHTML) {
hasChildrenOverride = true
push(props.innerHTML)
} else if (props.textContent) {
hasChildrenOverride = true
push(escapeHtml(props.textContent))
} else if (tag === 'textarea' && props.value) {
hasChildrenOverride = true
push(escapeHtml(props.value))
}
}
if (!hasChildrenOverride) {
if (shapeFlag & ShapeFlags.TEXT_CHILDREN) {
push(escapeHtml(children as string))
} else if (shapeFlag & ShapeFlags.ARRAY_CHILDREN) {
renderVNodeChildren(
push,
children as VNodeArrayChildren,
parentComponent,
slotScopeId,
)
}
}
push(`</${tag}>`)
}
}异步组件与 Suspense
异步组件处理
在 SSR 中,异步组件会被等待解析后再渲染:
typescript
// 异步组件示例
const AsyncComponent = {
async setup() {
// 异步数据获取
const data = await fetchData()
return () => h('div', data)
},
}
// 在 renderComponentVNode 中的处理
if (hasAsyncSetup || prefetches) {
const p: Promise<unknown> = Promise.resolve(res as Promise<void>)
.then(() => {
if (hasAsyncSetup) prefetches = instance.sp
if (prefetches) {
// 执行 serverPrefetch 钩子
return Promise.all(
prefetches.map(prefetch => prefetch.call(instance.proxy)),
)
}
})
.catch(NOOP)
return p.then(() => renderComponentSubTree(instance, slotScopeId))
}Suspense 在 SSR 中的实现
typescript
export async function ssrRenderSuspense(
push: PushFn,
{ default: renderContent }: Record<string, (() => void) | undefined>,
): Promise<void> {
if (renderContent) {
renderContent() // 直接渲染默认内容
} else {
push(`<!---->`) // 空注释
}
}SSR 中的 Suspense 行为与客户端不同:
- 服务端:等待所有异步组件解析完成,直接渲染最终内容
- 客户端:先显示 fallback,异步组件解析后替换内容
错误处理
typescript
// 测试用例展示错误处理
const RejectingAsync = {
setup() {
return new Promise((_, reject) => reject('error'))
},
}
// 渲染结果:组件渲染失败时输出空注释
expect(await renderToString(createApp(RejectingAsync))).toBe(`<!---->`)流式渲染实现
流式渲染原理
流式渲染允许在组件渲染完成时立即发送 HTML 片段,而不需要等待整个页面渲染完成:
typescript
export function renderToSimpleStream<T extends SimpleReadable>(
input: App | VNode,
context: SSRContext,
stream: T,
): T {
if (isVNode(input)) {
return renderToSimpleStream(
createApp({ render: () => input }),
context,
stream,
)
}
const vnode = createVNode(input._component, input._props)
vnode.appContext = input._context
input.provide(ssrContextKey, context)
Promise.resolve(renderComponentVNode(vnode))
.then(buffer => unrollBuffer(buffer, stream)) // 流式展开缓冲区
.then(() => resolveTeleports(context))
.then(() => {
if (context.__watcherHandles) {
for (const unwatch of context.__watcherHandles) {
unwatch()
}
}
})
.then(() => stream.push(null)) // 结束流
.catch(error => {
stream.destroy(error) // 错误处理
})
return stream
}流式缓冲区展开
typescript
async function unrollBuffer(
buffer: SSRBuffer,
stream: SimpleReadable,
): Promise<void> {
if (buffer.hasAsync) {
// 异步缓冲区:逐个等待并推送
for (let i = 0; i < buffer.length; i++) {
let item = buffer[i]
if (isPromise(item)) {
item = await item
}
if (isString(item)) {
stream.push(item) // 立即推送字符串
} else {
await unrollBuffer(item, stream) // 递归处理嵌套缓冲区
}
}
} else {
// 同步缓冲区:直接展开
unrollBufferSync(buffer, stream)
}
}
function unrollBufferSync(buffer: SSRBuffer, stream: SimpleReadable) {
for (let i = 0; i < buffer.length; i++) {
let item = buffer[i]
if (isString(item)) {
stream.push(item)
} else {
unrollBufferSync(item as SSRBuffer, stream)
}
}
}多种流式 API
Vue 3 提供了多种流式渲染 API 以适应不同环境:
Node.js Streams
typescript
export function renderToNodeStream(
input: App | VNode,
context: SSRContext = {},
): Readable {
const stream: Readable = __CJS__
? new (require('node:stream').Readable)({ read() {} })
: null
if (!stream) {
throw new Error(
`ESM build of renderToStream() does not support renderToNodeStream(). ` +
`Use pipeToNodeWritable() with an existing Node.js Writable stream ` +
`instance instead.`,
)
}
return renderToSimpleStream(input, context, stream)
}
export function pipeToNodeWritable(
input: App | VNode,
context: SSRContext | undefined = {},
writable: Writable,
): void {
renderToSimpleStream(input, context, {
push(content) {
if (content != null) {
writable.write(content)
} else {
writable.end()
}
},
destroy(err) {
writable.destroy(err)
},
})
}Web Streams
typescript
export function renderToWebStream(
input: App | VNode,
context: SSRContext = {},
): ReadableStream {
if (typeof ReadableStream !== 'function') {
throw new Error(
`ReadableStream constructor is not available in the global scope.`
)
}
const encoder = new TextEncoder()
let cancelled = false
return new ReadableStream({
start(controller) {
renderToSimpleStream(input, context, {
push(content) {
if (cancelled) return
if (content != null) {
controller.enqueue(encoder.encode(content))
} else {
controller.close()
}
},
destroy(err) {
controller.error(err)
},
})
},
cancel() {
cancelled = true
},
})
}
export function pipeToWebWritable(
input: App | VNode,
context: SSRContext | undefined = {},
writable: WritableStream,
): void {
const writer = writable.getWriter()
const encoder = new TextEncoder()
// CloudFlare Workers 兼容性处理
let hasReady = false
try {
hasReady = isPromise(writer.ready)
} catch (e: any) {}
renderToSimpleStream(input, context, {
async push(content) {
if (hasReady) {
await writer.ready
}
if (content != null) {
return writer.write(encoder.encode(content))
} else {
return writer.close()
}
},
destroy(err) {
console.log(err)
writer.close()
},
})
}服务端模板编译
编译缓存机制
typescript
const compileCache: Record<string, SSRRenderFunction> = Object.create(null)
export function ssrCompile(
template: string,
instance: ComponentInternalInstance,
): SSRRenderFunction {
// ESM 构建不支持运行时编译
if (!__CJS__) {
throw new Error(
`On-the-fly template compilation is not supported in the ESM build of ` +
`@vue/server-renderer. All templates must be pre-compiled into ` +
`render functions.`,
)
}
const Component = instance.type as ComponentOptions
const { isCustomElement, compilerOptions } = instance.appContext.config
const { delimiters, compilerOptions: componentCompilerOptions } = Component
// 合并编译选项
const finalCompilerOptions: CompilerOptions = extend(
extend(
{
isCustomElement,
delimiters,
},
compilerOptions,
),
componentCompilerOptions,
)
finalCompilerOptions.isCustomElement =
finalCompilerOptions.isCustomElement || NO
finalCompilerOptions.isNativeTag = finalCompilerOptions.isNativeTag || NO
// 生成缓存键
const cacheKey = JSON.stringify(
{
template,
compilerOptions: finalCompilerOptions,
},
(key, value) => {
return isFunction(value) ? value.toString() : value
},
)
// 检查缓存
const cached = compileCache[cacheKey]
if (cached) {
return cached
}
// 错误处理
finalCompilerOptions.onError = (err: CompilerError) => {
if (__DEV__) {
const message = `[@vue/server-renderer] Template compilation error: ${err.message}`
const codeFrame =
err.loc &&
generateCodeFrame(
template as string,
err.loc.start.offset,
err.loc.end.offset,
)
warn(codeFrame ? `${message}\n${codeFrame}` : message)
} else {
throw err
}
}
// 编译模板
const { code } = compile(template, finalCompilerOptions)
// 创建模拟的 require 函数
const requireMap = {
vue: Vue,
'vue/server-renderer': helpers,
}
const fakeRequire = (id: 'vue' | 'vue/server-renderer') => requireMap[id]
// 缓存并返回编译结果
return (compileCache[cacheKey] = Function('require', code)(fakeRequire))
}Teleport 处理
Teleport 渲染
typescript
function renderTeleportVNode(
push: PushFn,
vnode: VNode,
parentComponent: ComponentInternalInstance,
slotScopeId?: string,
) {
const target = vnode.props && vnode.props.to
const disabled = vnode.props && vnode.props.disabled
if (!target) {
if (!disabled) {
warn(`[@vue/server-renderer] Teleport is missing target prop.`)
}
return []
}
if (!isString(target)) {
warn(
`[@vue/server-renderer] Teleport target must be a query selector string.`,
)
return []
}
ssrRenderTeleport(
push,
push => {
renderVNodeChildren(
push,
vnode.children as VNodeArrayChildren,
parentComponent,
slotScopeId,
)
},
target,
disabled || disabled === '',
parentComponent,
)
}Teleport 解析
typescript
export async function resolveTeleports(context: SSRContext): Promise<void> {
if (context.__teleportBuffers) {
context.teleports = context.teleports || {}
for (const key in context.__teleportBuffers) {
// 注意:这里可以顺序等待,因为 Promise 是并行创建的
context.teleports[key] = await unrollBuffer(
await Promise.all([context.__teleportBuffers[key]]),
)
}
}
}性能优化策略
1. 渲染缓存
组件级缓存
typescript
// 组件缓存示例
const CachedComponent = {
name: 'CachedComponent',
serverCacheKey: (props) => props.id,
render() {
return h('div', `Cached content for ${this.id}`)
}
}模板编译缓存
typescript
// ssrCompile.ts 中的缓存机制
const compileCache: Record<string, SSRRenderFunction> = Object.create(null)
// 基于模板和编译选项生成缓存键
const cacheKey = JSON.stringify({
template,
compilerOptions: finalCompilerOptions,
})
const cached = compileCache[cacheKey]
if (cached) {
return cached // 直接返回缓存的渲染函数
}2. 流式输出优化
减少 TTFB(Time To First Byte)
typescript
// 流式渲染立即开始输出
Promise.resolve(renderComponentVNode(vnode))
.then(buffer => unrollBuffer(buffer, stream)) // 边渲染边输出
.then(() => stream.push(null)) // 完成时关闭流异步组件优化
typescript
// 异步组件并行处理
if (prefetches) {
return Promise.all(
prefetches.map(prefetch => prefetch.call(instance.proxy)),
)
}3. 字符串合并优化
typescript
// createBuffer 中的字符串合并
push(item: SSRBufferItem): void {
const isStringItem = isString(item)
// 连续的字符串直接合并,减少数组长度
if (appendable && isStringItem) {
buffer[buffer.length - 1] += item as string
return
}
buffer.push(item)
appendable = isStringItem
}4. 作用域 ID 优化
typescript
// 延迟克隆属性对象
let attrs = instance.inheritAttrs !== false ? instance.attrs : undefined
let hasCloned = false
// 只在需要时才克隆
if (scopeId) {
if (!hasCloned) {
attrs = { ...attrs }
hasCloned = true
}
attrs![scopeId] = ''
}错误处理与调试
开发模式增强
typescript
if (__DEV__) {
// 警告上下文管理
pushWarningContext(vnode)
// ... 渲染逻辑
popWarningContext()
// 详细错误信息
const message = `[@vue/server-renderer] Template compilation error: ${err.message}`
const codeFrame = err.loc && generateCodeFrame(
template as string,
err.loc.start.offset,
err.loc.end.offset,
)
warn(codeFrame ? `${message}\n${codeFrame}` : message)
}生产模式优化
typescript
if (__DEV__) {
// 开发模式的详细错误处理
} else {
throw err // 生产模式直接抛出错误
}与客户端渲染的差异
1. 生命周期差异
| 生命周期钩子 | 客户端 | 服务端 |
|---|---|---|
| beforeCreate | ✅ | ✅ |
| created | ✅ | ✅ |
| beforeMount | ✅ | ❌ |
| mounted | ✅ | ❌ |
| serverPrefetch | ❌ | ✅ |
2. 响应式系统
- 服务端:响应式数据在渲染时是静态的,不会触发更新
- 客户端:响应式数据变化会触发重新渲染
3. 事件处理
- 服务端:事件监听器不会被注册,只渲染静态 HTML
- 客户端:事件监听器正常工作
4. DOM 操作
- 服务端:无法访问 DOM API
- 客户端:可以正常使用 DOM API
最佳实践
1. 组件设计
typescript
// ✅ 好的 SSR 组件设计
const SSRFriendlyComponent = {
async serverPrefetch() {
// 在服务端预取数据
this.data = await fetchData()
},
data() {
return {
data: null
}
},
async mounted() {
// 客户端激活时的逻辑
if (!this.data) {
this.data = await fetchData()
}
}
}2. 异步数据处理
typescript
// ✅ 使用 serverPrefetch 预取数据
export default {
async serverPrefetch() {
// 服务端数据预取
const data = await this.$store.dispatch('fetchData')
return data
},
async created() {
// 客户端数据获取
if (process.client && !this.$store.state.data) {
await this.$store.dispatch('fetchData')
}
}
}3. 条件渲染
typescript
// ✅ 使用 ClientOnly 组件包装客户端特定内容
const App = {
render() {
return h('div', [
h('h1', 'Universal Content'),
h(ClientOnly, () => h('div', 'Client Only Content'))
])
}
}4. 性能监控
typescript
// ✅ 添加性能监控
const startTime = Date.now()
const html = await renderToString(app, context)
const renderTime = Date.now() - startTime
console.log(`SSR render time: ${renderTime}ms`)总结
Vue 3 的 SSR 实现展现了现代前端框架在服务端渲染方面的技术深度:
- 高效的渲染机制:通过缓冲区系统和流式渲染实现高性能输出
- 完善的异步处理:支持异步组件、Suspense 和 serverPrefetch
- 灵活的流式 API:支持 Node.js Streams 和 Web Streams
- 智能的优化策略:编译缓存、字符串合并、作用域 ID 优化
- 强大的错误处理:开发模式的详细错误信息和生产模式的性能优化
SSR 的核心价值在于:
- 更快的首屏加载:服务端直接输出 HTML
- 更好的 SEO:搜索引擎可以直接索引内容
- 更佳的用户体验:减少白屏时间
- 渐进式增强:支持 JavaScript 禁用的环境
Vue 3 的 SSR 实现不仅保持了框架的易用性,还在性能和功能方面达到了工业级标准,为构建现代 Web 应用提供了强有力的技术支撑。
