Principles of SSR Isomorphic Rendering with Virtual DOM
Description
SSR isomorphic rendering with Virtual DOM refers to the process where the same codebase generates HTML strings on the server side and then "activates" (Hydrates) them on the client side via Virtual DOM. This solves the problem of traditional server-side rendering being unable to preserve interactive states.
Core Issues
- How does the server render components into HTML strings?
- How does the client reuse server-side HTML and add event bindings?
- How to avoid mismatching errors during the hydration process?
Server-Side Rendering Process
Step 1: Serializing Components into Strings
// Server uses renderToString to convert Virtual DOM into string
import { renderToString } from 'vue/server-renderer'
const app = createApp(App)
const html = await renderToString(app)
// Output: <div data-server-rendered="true">Hello</div>
- Virtual DOM generates plain text HTML through recursive traversal
- Adds
data-server-renderedmarker for client-side identification
Step 2: Handling Asynchronous Components
// Server needs to wait for all asynchronous operations to complete
const app = createApp({
async setup() {
const data = await fetchData() // Wait for async data
return { data }
}
})
- Server-side rendering deeply traverses all nested components
- Ensures all asynchronous operations complete before outputting HTML
Client-Side Hydration Process
Step 3: Hydration
// Client uses createSSRApp for hydration
const app = createSSRApp(App)
app.mount('#app') // Not recreating DOM, but hydrating existing DOM
Hydration Process Details:
- DOM Reuse Check: Compares Virtual DOM structure with server-side HTML
- Attribute Synchronization: Adds server-side unserialized attributes (e.g., events) to DOM
- Event Binding: Attaches event listeners to DOM elements
Step 4: Virtual DOM Diffing Algorithm
function hydrate(vnode, container) {
const existingDOM = container.firstChild // Get existing DOM
// Compare tag names and key attributes
if (vnode.tag !== existingDOM.tagName.toLowerCase()) {
// Perform client-side rendering when mismatch occurs
renderClient(vnode, container)
return
}
// Recursively process child nodes
for (let i = 0; i < vnode.children.length; i++) {
hydrate(vnode.children[i], existingDOM.childNodes[i])
}
// Add client-specific logic like event listeners
addEventListeners(vnode, existingDOM)
}
Key Optimization Strategies
Strategy 1: Text Node Exact Matching
// Text nodes require exact match for reuse
if (vnode.type === Text) {
if (existingDOM.nodeType === Node.TEXT_NODE) {
if (vnode.children !== existingDOM.textContent) {
// Replace when text content doesn't match
existingDOM.textContent = vnode.children
}
}
}
Strategy 2: Attribute Difference Handling
function hydrateProps(vnode, dom) {
const serverProps = getServerProps(dom) // Get server-rendered attributes
const clientProps = vnode.props // Client-expected attributes
// Merge strategy: Client-side takes precedence, but preserves server-side key attributes
for (const key in clientProps) {
if (key.startsWith('on')) {
// Event handlers only added on client side
dom.addEventListener(key.slice(2).toLowerCase(), clientProps[key])
} else if (serverProps[key] !== clientProps[key]) {
// Client-side takes precedence when attributes differ
dom.setAttribute(key, clientProps[key])
}
}
}
Common Issues and Solutions
Issue 1: Client-Server State Inconsistency
// Bad example: Using Date.now() causes mismatch
const timestamp = Date.now() // Different values on server and client
// Solution: Set dynamic values after mounted
const timestamp = ref(0)
onMounted(() => {
timestamp.value = Date.now()
})
Issue 2: SSR Support for Third-Party Libraries
// Check if in browser environment
if (typeof window !== 'undefined') {
// Execute only on client side
import('client-only-library').then(module => {
// Use client-only library
})
}
Performance Optimization Techniques
Technique 1: Component-Level Hydration
<template>
<div>
<!-- Static content directly reused -->
<StaticContent />
<!-- Dynamic content with deferred hydration -->
<ClientOnly>
<InteractiveComponent />
</ClientOnly>
</div>
</template>
<script>
export default {
components: {
ClientOnly: {
mounted() {
// Lazy load interactive components
this.$el.hydrate()
}
}
}
}
</script>
Technique 2: Streaming Rendering Optimization
// Server-side streaming rendering, sending HTML progressively
const stream = renderToNodeStream(app)
stream.pipe(res)
// Progressive client-side hydration, improving First Contentful Paint
const hydrator = createHydrator(stream)
hydrator.activateChunk(chunk)
Summary
SSR isomorphic rendering achieves perfect integration of server-side first-screen rendering and client-side interactive capabilities through precise Virtual DOM comparison and intelligent hydration mechanisms. The key lies in ensuring state consistency between server and client, and optimizing the performance of the hydration process.