Vue.js server-side rendering (SSR)

What is SSR?

Server-Side Rendering (SSR) refers to rendering Vue components into HTML strings on the server and then sending them to the client. This is different from client-side rendering (CSR), which renders components in the browser.

Advantages of SSR

1. Better SEO

Search engine crawlers can directly see the complete page content.

2. Faster first screen loading

Users can see page content faster without having to wait for JavaScript to download and execute.

3. Better performance (in some cases)

For content-intensive applications, SSR can provide better performance.

Disadvantages of SSR

  • Higher server load
  • Increased development complexity
  • Higher deployment and maintenance costs
  • Some browser APIs are not available

Nuxt.js is a Vue-based SSR framework that provides out-of-the-box SSR support.

Install Nuxt

npx nuxi@latest init my-app
cd my-app
npm install
npm run dev

Nuxt project structure

my-app/
├── pages/          # 页面(自动路由)
├── components/     # 组件
├── layouts/        # 布局
├── composables/    # 组合式函数
├── plugins/        # 插件
├── middleware/     # 中间件
├── public/         # 静态资源
├── server/         # 服务端代码
└── nuxt.config.ts  # 配置文件

Create page

<!-- pages/index.vue -->
<script setup>
const title = 'Welcome to Nuxt!'
</script>

<template>
  <div>
    <h1>{{ title }}</h1>
    <p>This page is server-rendered!</p>
  </div>
</template>

Data acquisition

<!-- pages/posts/[id].vue -->
<script setup>
const route = useRoute()
const { data: post } = await useFetch(`/api/posts/${route.params.id}`)
</script>

<template>
  <div>
    <h1>{{ post.title }}</h1>
    <p>{{ post.content }}</p>
  </div>
</template>

Manually implement SSR

If you are not using Nuxt, you can implement SSR manually:

1. Install dependencies

npm install express vue @vue/server-renderer

2. Create server

// server.js
import express from 'express'
import { createSSRApp } from 'vue'
import { renderToString } from '@vue/server-renderer'

const server = express()

server.get('*', async (req, res) => {
  const app = createSSRApp({
    data() {
      return {
        message: 'Hello from SSR!'
      }
    },
    template: `<div>{{ message }}</div>`
  })
  
  const html = await renderToString(app)
  
  res.send(`
    <!DOCTYPE html>
    <html>
      <head>
        <title>Vue SSR</title>
      </head>
      <body>
        <div id="app">${html}</div>
      </body>
    </html>
  `)
})

server.listen(3000, () => {
  console.log('Server running at http://localhost:3000')
})

3. Client activation (Hydration)

// client.js
import { createSSRApp } from 'vue'

const app = createSSRApp({
  data() {
    return {
      message: 'Hello from SSR!'
    }
  },
  template: `<div>{{ message }}</div>`
})

app.mount('#app')

SSR data prefetching

<!-- pages/users.vue -->
<script setup>
// 服务端和客户端都会执行
const { data: users, pending, error } = await useFetch('/api/users')
</script>

<template>
  <div>
    <div v-if="pending">Loading...</div>
    <div v-else-if="error">Error: {{ error.message }}</div>
    <div v-else>
      <h1>Users</h1>
      <ul>
        <li v-for="user in users" :key="user.id">
          {{ user.name }}
        </li>
      </ul>
    </div>
  </div>
</template>

SSR life cycle

In SSR, onlybeforeCreateandcreatedThe hook will be executed on the server side:

<script setup>
import { onMounted, onServerPrefetch } from 'vue'

// 只在服务端执行
onServerPrefetch(async () => {
  console.log('Server prefetch')
  // 预取数据
})

// 只在客户端执行
onMounted(() => {
  console.log('Client mounted')
  // 客户端逻辑
})
</script>

Handle browser API

Some browser APIs are not available on the server side:

<script setup>
import { ref, onMounted } from 'vue'

const windowWidth = ref(0)

// ❌ 错误:window 在服务端不存在
// const windowWidth = ref(window.innerWidth)

// ✅ 正确:在客户端钩子中使用
onMounted(() => {
  windowWidth.value = window.innerWidth
})

// 或者使用条件判断
if (typeof window !== 'undefined') {
  windowWidth.value = window.innerWidth
}
</script>

State management and SSR

Pinia with SSR

// stores/user.js
import { defineStore } from 'pinia'

export const useUserStore = defineStore('user', {
  state: () => ({
    user: null
  }),
  actions: {
    async fetchUser() {
      // 服务端和客户端都会执行
      const response = await fetch('/api/user')
      this.user = await response.json()
    }
  }
})
<!-- pages/profile.vue -->
<script setup>
const userStore = useUserStore()

// 服务端预取数据
await userStore.fetchUser()
</script>

<template>
  <div>
    <h1>{{ userStore.user?.name }}</h1>
  </div>
</template>

Caching strategy

Page level caching

// nuxt.config.ts
export default defineNuxtConfig({
  routeRules: {
    '/': { prerender: true },  // 预渲染
    '/api/**': { cache: { maxAge: 60 } },  // API 缓存
    '/admin/**': { ssr: false }  // 禁用 SSR
  }
})

Component level caching

<script setup>
// 缓存组件 60 秒
defineOptions({
  cache: {
    maxAge: 60
  }
})
</script>

Static site generation (SSG)

Nuxt supports static site generation:

# 生成静态站点
npm run generate
// nuxt.config.ts
export default defineNuxtConfig({
  ssr: true,
  target: 'static'
})

Mixed rendering

Different rendering modes can be selected for different routes:

// nuxt.config.ts
export default defineNuxtConfig({
  routeRules: {
    '/': { prerender: true },           // SSG
    '/blog/**': { swr: 3600 },          // ISR (增量静态再生)
    '/admin/**': { ssr: false },        // CSR
    '/api/**': { cors: true }           // API
  }
})

SEO Optimization

<!-- pages/blog/[id].vue -->
<script setup>
const route = useRoute()
const { data: post } = await useFetch(`/api/posts/${route.params.id}`)

// 设置 meta 标签
useHead({
  title: post.value.title,
  meta: [
    { name: 'description', content: post.value.excerpt },
    { property: 'og:title', content: post.value.title },
    { property: 'og:description', content: post.value.excerpt },
    { property: 'og:image', content: post.value.image }
  ]
})
</script>

<template>
  <article>
    <h1>{{ post.title }}</h1>
    <div v-html="post.content"></div>
  </article>
</template>

Performance optimization

1. Code splitting

<script setup>
// 懒加载组件
const HeavyComponent = defineAsyncComponent(() =>
  import('./HeavyComponent.vue')
)
</script>

2. Prefetch key resources

<script setup>
useHead({
  link: [
    { rel: 'preload', href: '/fonts/main.woff2', as: 'font' },
    { rel: 'prefetch', href: '/api/data' }
  ]
})
</script>

3. Use CDN

// nuxt.config.ts
export default defineNuxtConfig({
  app: {
    cdnURL: 'https://cdn.example.com'
  }
})

Deployment

Vercel

# 安装 Vercel CLI
npm i -g vercel

# 部署
vercel

Netlify

# 构建命令
npm run build

# 输出目录
.output/public

Node.js Server

# 构建
npm run build

# 启动
node .output/server/index.mjs

Summarize

  • SSR provides better SEO and above-the-fold performance
  • Nuxt.js is the best choice for Vue SSR
  • Pay attention to the differences between server and client
  • Proper use of caching strategies
  • Can mix SSR, SSG and CSR -Choose appropriate rendering modes for different scenarios

Next step

Next, learn Toolchain and Ecosystem to learn about the tools and libraries in the Vue ecosystem.