Principles of SSR Isomorphic Rendering with Virtual DOM

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

  1. How does the server render components into HTML strings?
  2. How does the client reuse server-side HTML and add event bindings?
  3. 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-rendered marker 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:

  1. DOM Reuse Check: Compares Virtual DOM structure with server-side HTML
  2. Attribute Synchronization: Adds server-side unserialized attributes (e.g., events) to DOM
  3. 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.