How to Implement Server-Side Rendering in Vue.js

Implement Server-Side Rendering (SSR) in Vue.js with our step-by-step guide. Boost your Vue.js app's performance, SEO, and user experience.

Server-side rendering (SSR) can significantly improve the performance and SEO of your Vue.js applications. By rendering the initial HTML on the server, you can deliver content to users faster, enhancing their experience and improving search engine indexing. In this article, we will guide you through the process of implementing SSR in Vue.js, providing detailed, step-by-step instructions to ensure your application performs at its best from the moment users land on your page.

Understanding Server-Side Rendering in Vue.js

Server-side rendering is a technique where your server generates the HTML for a web page and sends it to the client. Unlike client-side rendering, which relies on the browser to build the HTML using JavaScript, SSR delivers a fully rendered page directly from the server. This results in faster initial load times and better performance, especially for users on slower networks or devices.

What is Server-Side Rendering?

Server-side rendering is a technique where your server generates the HTML for a web page and sends it to the client. Unlike client-side rendering, which relies on the browser to build the HTML using JavaScript, SSR delivers a fully rendered page directly from the server.

This results in faster initial load times and better performance, especially for users on slower networks or devices.

Why Use SSR in Vue.js?

Implementing SSR in Vue.js brings several benefits. First, it improves the user experience by reducing the time it takes for the content to appear on the screen. Second, it enhances SEO since search engines can easily crawl and index the server-rendered HTML.

Finally, SSR can improve accessibility and performance for users on slower connections, ensuring your application reaches a wider audience.

Setting Up Your Vue.js Project for SSR

Initial Project Setup

To get started with SSR in Vue.js, you first need to set up a Vue.js project. You can use Vue CLI to create a new project. Open your terminal and run the following command:

vue create my-ssr-app

Choose the default preset or customize it according to your needs. Once the project is created, navigate into the project directory:

cd my-ssr-app

Installing Necessary Dependencies

SSR requires additional dependencies to handle server-side rendering. Install the vue-server-renderer package:

npm install vue-server-renderer

This package provides the necessary tools to render your Vue components on the server.

Creating the Server Entry File

Next, create an entry file for your server-side application. In the root directory of your project, create a file named server.js:

touch server.js

Open server.js and add the following code to set up a basic Express server:

const express = require('express');
const { createBundleRenderer } = require('vue-server-renderer');
const server = express();

server.get('*', (req, res) => {
  res.send('Hello from server-side rendering');
});

server.listen(8080, () => {
  console.log('Server is running on http://localhost:8080');
});

This code initializes an Express server that listens on port 8080 and responds with a simple message. You will expand on this basic setup to include SSR.

Configuring Webpack for SSR

To build your Vue.js application for SSR, you need to configure Webpack to generate both client and server bundles. Create two new files in the root directory: webpack.client.config.js and webpack.server.config.js.

webpack.client.config.js

Add the following code to configure the client-side build:

const { merge } = require('webpack-merge');
const baseConfig = require('./webpack.base.config');
const VueSSRClientPlugin = require('vue-server-renderer/client-plugin');

module.exports = merge(baseConfig, {
  entry: './src/entry-client.js',
  plugins: [
    new VueSSRClientPlugin()
  ]
});

webpack.server.config.js

Add the following code to configure the server-side build:

const { merge } = require('webpack-merge');
const baseConfig = require('./webpack.base.config');
const VueSSRServerPlugin = require('vue-server-renderer/server-plugin');

module.exports = merge(baseConfig, {
  entry: './src/entry-server.js',
  target: 'node',
  output: {
    libraryTarget: 'commonjs2'
  },
  plugins: [
    new VueSSRServerPlugin()
  ]
});

Modifying Your Vue Application for SSR

To enable SSR, you need to create separate entry files for the client and server. In the src directory, create two new files: entry-client.js and entry-server.js.

entry-client.js

This file is the entry point for the client-side build:

import { createApp } from './main';

const { app, router } = createApp();

router.onReady(() => {
  app.$mount('#app');
});

entry-server.js

This file is the entry point for the server-side build:

import { createApp } from './main';

export default context => {
  return new Promise((resolve, reject) => {
    const { app, router } = createApp();

    router.push(context.url);

    router.onReady(() => {
      resolve(app);
    }, reject);
  });
};

Integrating Server-Side Rendering with Vue.js

Now that you have separate entry files for the client and server, you need to modify the main entry point of your Vue.js application to work with SSR. In the src directory, create a new file called main.js:

Setting Up the Main Application Entry Point

Now that you have separate entry files for the client and server, you need to modify the main entry point of your Vue.js application to work with SSR. In the src directory, create a new file called main.js:

main.js

import Vue from 'vue';
import App from './App.vue';
import { createRouter } from './router';

export function createApp() {
  const router = createRouter();
  const app = new Vue({
    router,
    render: h => h(App)
  });
  return { app, router };
}

In this setup, you export a createApp function that returns the Vue instance and the router. This function will be called by both the client and server entry points.

Setting Up Routing for SSR

Routing is an essential part of a single-page application, and it needs to work seamlessly with SSR. Create a router.js file in the src directory to define your routes:

router.js

import Vue from 'vue';
import Router from 'vue-router';
import Home from './components/Home.vue';
import About from './components/About.vue';

Vue.use(Router);

export function createRouter() {
  return new Router({
    mode: 'history',
    routes: [
      { path: '/', component: Home },
      { path: '/about', component: About }
    ]
  });
}

This code sets up basic routing with two routes: Home and About. The createRouter function is called by the main entry point to create a new router instance.

Updating the Server Configuration

Next, update your server configuration in server.js to use the Vue server renderer. This setup will render the application on the server and send the fully rendered HTML to the client.

server.js

const express = require('express');
const fs = require('fs');
const path = require('path');
const { createBundleRenderer } = require('vue-server-renderer');
const server = express();

const clientManifest = require('./dist/vue-ssr-client-manifest.json');
const serverBundle = require('./dist/vue-ssr-server-bundle.json');
const template = fs.readFileSync(path.resolve(__dirname, 'index.template.html'), 'utf-8');

const renderer = createBundleRenderer(serverBundle, {
  runInNewContext: false,
  template,
  clientManifest
});

server.use('/dist', express.static(path.join(__dirname, './dist')));

server.get('*', (req, res) => {
  const context = { url: req.url };

  renderer.renderToString(context, (err, html) => {
    if (err) {
      if (err.code === 404) {
        res.status(404).send('Page not found');
      } else {
        res.status(500).send('Internal Server Error');
      }
    } else {
      res.send(html);
    }
  });
});

server.listen(8080, () => {
  console.log('Server is running on http://localhost:8080');
});

Creating an HTML Template

Create an HTML template that the server renderer will use to inject the rendered Vue.js application. In the root directory, create a file named index.template.html:

index.template.html

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <title>Vue SSR App</title>
  <link rel="stylesheet" href="/dist/style.css">
</head>
<body>
  <!--vue-ssr-outlet-->
</body>
</html>

The <!--vue-ssr-outlet--> comment is a placeholder where the server-rendered content will be injected.

Building and Running the Application

Now that your setup is complete, you need to build the client and server bundles and start the server.

Building the Client and Server Bundles

Add the following scripts to your package.json file:

"scripts": {
  "build:client": "webpack --config webpack.client.config.js",
  "build:server": "webpack --config webpack.server.config.js",
  "build": "npm run build:client && npm run build:server",
  "start": "node server.js"
}

Run the build script to generate the client and server bundles:

npm run build

Starting the Server

After building the bundles, start the server:

npm start

Your server-side rendered Vue.js application should now be running on http://localhost:8080.

Enhancing Performance and SEO with SSR

Performance Optimization Techniques

Implementing SSR is a significant step towards better performance, but additional optimizations can further enhance the speed and responsiveness of your application.

Caching Strategies

Implement server-side caching to store rendered pages and reduce the load on your server. Tools like Redis can help cache the HTML output, ensuring subsequent requests for the same content are served quickly.

Lazy Loading

Use lazy loading to defer the loading of non-essential components until they are needed. This technique reduces the initial payload and speeds up the first render. In Vue.js, you can use dynamic imports to implement lazy loading.

const Home = () => import('./components/Home.vue');
const About = () => import('./components/About.vue');

Minification and Compression

Ensure your JavaScript and CSS files are minified and compressed to reduce their size. Use tools like Webpack’s TerserPlugin for JavaScript minification and CSSNano for CSS minification. Enable Gzip or Brotli compression on your server to further reduce the size of the files sent to the client.

SEO Benefits of SSR

SSR can significantly improve the SEO of your Vue.js application by providing fully rendered HTML to search engines. This allows search engines to crawl and index your content more effectively.

Meta Tags and Open Graph

Ensure your pages include relevant meta tags and Open Graph tags for better SEO and social media sharing. Use Vue Meta or Nuxt.js’s built-in head management to dynamically set meta tags based on the page content.

export default {
  head() {
    return {
      title: 'Home Page',
      meta: [
        { name: 'description', content: 'This is the home page of our Vue SSR app' },
        { property: 'og:title', content: 'Home Page' },
        { property: 'og:description', content: 'This is the home page of our Vue SSR app' }
      ]
    };
  }
};

Enhancing User Experience with SSR

One of the primary benefits of SSR is faster initial load times. By rendering the initial HTML on the server, users see content more quickly, which enhances their experience and reduces bounce rates.

Faster Initial Load Times

One of the primary benefits of SSR is faster initial load times. By rendering the initial HTML on the server, users see content more quickly, which enhances their experience and reduces bounce rates.

Implementing Progressive Hydration

Progressive hydration is a technique where the server-rendered HTML is gradually enhanced with JavaScript. This approach allows users to interact with the page sooner, even as additional JavaScript functionality is still loading.

Vue.js supports progressive hydration out of the box, enabling you to deliver a fast, interactive experience from the start.

Handling Asynchronous Data

Efficiently managing asynchronous data is crucial for a smooth user experience with SSR. When the server renders the initial HTML, it must also handle data fetching to ensure the content is up-to-date and accurate.

Using Vuex for State Management

Vuex is a state management library for Vue.js that helps manage the state of your application. It is particularly useful for SSR, as it allows you to fetch data on the server and pass the initial state to the client.

First, install Vuex:

npm install vuex

Next, create a store in src/store.js:

store.js

import Vue from 'vue';
import Vuex from 'vuex';

Vue.use(Vuex);

export function createStore() {
  return new Vuex.Store({
    state: {
      message: ''
    },
    actions: {
      fetchMessage({ commit }) {
        // Simulate an API call
        return new Promise((resolve) => {
          setTimeout(() => {
            commit('setMessage', 'Hello from Vuex store!');
            resolve();
          }, 1000);
        });
      }
    },
    mutations: {
      setMessage(state, message) {
        state.message = message;
      }
    }
  });
}

Modify main.js to include the store:

main.js

import Vue from 'vue';
import App from './App.vue';
import { createRouter } from './router';
import { createStore } from './store';

export function createApp() {
  const router = createRouter();
  const store = createStore();
  const app = new Vue({
    router,
    store,
    render: h => h(App)
  });
  return { app, router, store };
}

In entry-server.js, handle data pre-fetching:

entry-server.js

import { createApp } from './main';

export default context => {
  return new Promise((resolve, reject) => {
    const { app, router, store } = createApp();

    router.push(context.url);

    router.onReady(() => {
      const matchedComponents = router.getMatchedComponents();
      if (!matchedComponents.length) {
        return reject({ code: 404 });
      }

      Promise.all(matchedComponents.map(Component => {
        if (Component.asyncData) {
          return Component.asyncData({ store, route: router.currentRoute });
        }
      })).then(() => {
        context.state = store.state;
        resolve(app);
      }).catch(reject);
    }, reject);
  });
};

In entry-client.js, hydrate the store with the initial state:

entry-client.js

import { createApp } from './main';

const { app, router, store } = createApp();

if (window.__INITIAL_STATE__) {
  store.replaceState(window.__INITIAL_STATE__);
}

router.onReady(() => {
  app.$mount('#app');
});

Using Vue Meta for SEO Enhancements

SEO is a crucial aspect of web development, and SSR can significantly enhance it. Vue Meta is a library that helps manage meta tags in Vue.js applications, ensuring that your pages are optimized for search engines and social media.

First, install Vue Meta:

npm install vue-meta

In main.js, add Vue Meta to your Vue instance:

main.js

import Vue from 'vue';
import App from './App.vue';
import { createRouter } from './router';
import { createStore } from './store';
import Meta from 'vue-meta';

Vue.use(Meta);

export function createApp() {
  const router = createRouter();
  const store = createStore();
  const app = new Vue({
    router,
    store,
    render: h => h(App),
    metaInfo: {
      titleTemplate: '%s - My Vue SSR App'
    }
  });
  return { app, router, store };
}

In your Vue components, use the metaInfo option to define meta tags:

Home.vue

export default {
  name: 'Home',
  metaInfo: {
    title: 'Home Page',
    meta: [
      { name: 'description', content: 'This is the home page of our Vue SSR app' },
      { property: 'og:title', content: 'Home Page' },
      { property: 'og:description', content: 'This is the home page of our Vue SSR app' }
    ]
  },
  asyncData({ store }) {
    return store.dispatch('fetchMessage');
  }
};

Implementing PWA Features with SSR

Progressive Web Apps (PWAs) offer an enhanced user experience by combining the best features of web and mobile apps. Implementing PWA features in your SSR application can provide offline access, push notifications, and improved performance.

First, install the required packages:

npm install @vue/cli-plugin-pwa

Next, configure your PWA settings in vue.config.js:

vue.config.js

module.exports = {
  pwa: {
    name: 'My Vue SSR App',
    themeColor: '#4DBA87',
    msTileColor: '#000000',
    manifestOptions: {
      background_color: '#42B883'
    }
  }
};

Create a service worker to manage caching and offline capabilities:

registerServiceWorker.js

import { register } from 'register-service-worker';

if (process.env.NODE_ENV === 'production') {
  register('/service-worker.js', {
    ready() {
      console.log('App is being served from cache by a service worker.');
    },
    registered() {
      console.log('Service worker has been registered.');
    },
    cached() {
      console.log('Content has been cached for offline use.');
    },
    updatefound() {
      console.log('New content is downloading.');
    },
    updated() {
      console.log('New content is available; please refresh.');
    },
    offline() {
      console.log('No internet connection found. App is running in offline mode.');
    },
    error(error) {
      console.error('Error during service worker registration:', error);
    }
  });
}

Import and register the service worker in your main.js:

main.js

import Vue from 'vue';
import App from './App.vue';
import { createRouter } from './router';
import { createStore } from './store';
import Meta from 'vue-meta';
import './registerServiceWorker';

Vue.use(Meta);

export function createApp() {
  const router = createRouter();
  const store = createStore();
  const app = new Vue({
    router,
    store,
    render: h => h(App),
    metaInfo: {
      titleTemplate: '%s - My Vue SSR App'
    }
  });
  return { app, router, store };
}

By implementing PWA features, you enhance the user experience by providing offline access, faster load times, and additional functionalities like push notifications.

Advanced Techniques for Enhancing SSR in Vue.js

Utilizing Webpack for Optimized Builds

Webpack is a powerful module bundler that can help optimize your Vue.js application for SSR. Efficiently configuring Webpack can reduce bundle sizes, improve load times, and enhance overall performance.

Webpack Configuration for SSR

In your webpack.server.config.js and webpack.client.config.js files, ensure you have the following configurations:

webpack.server.config.js
const path = require('path');
const { merge } = require('webpack-merge');
const baseConfig = require('./webpack.base.config');
const VueSSRServerPlugin = require('vue-server-renderer/server-plugin');

module.exports = merge(baseConfig, {
  entry: './src/entry-server.js',
  target: 'node',
  output: {
    path: path.resolve(__dirname, 'dist'),
    filename: 'server-bundle.js',
    libraryTarget: 'commonjs2'
  },
  externals: [nodeExternals()],
  plugins: [
    new VueSSRServerPlugin()
  ]
});
webpack.client.config.js
const path = require('path');
const { merge } = require('webpack-merge');
const baseConfig = require('./webpack.base.config');
const VueSSRClientPlugin = require('vue-server-renderer/client-plugin');

module.exports = merge(baseConfig, {
  entry: './src/entry-client.js',
  output: {
    path: path.resolve(__dirname, 'dist'),
    filename: 'client-bundle.js'
  },
  plugins: [
    new VueSSRClientPlugin()
  ],
  optimization: {
    splitChunks: {
      chunks: 'all'
    }
  }
});

Leveraging Modern JavaScript Features

Modern JavaScript features such as async/await, dynamic imports, and ES6+ syntax can help improve your SSR setup by making your code more efficient and easier to manage.

Using Async/Await for Data Fetching

Refactor your data fetching logic to use async/await for better readability and error handling.

store.js
import Vue from 'vue';
import Vuex from 'vuex';

Vue.use(Vuex);

export function createStore() {
  return new Vuex.Store({
    state: {
      message: ''
    },
    actions: {
      async fetchMessage({ commit }) {
        try {
          const message = await fetchMessageFromAPI();
          commit('setMessage', message);
        } catch (error) {
          console.error('Failed to fetch message:', error);
        }
      }
    },
    mutations: {
      setMessage(state, message) {
        state.message = message;
      }
    }
  });
}

async function fetchMessageFromAPI() {
  // Simulate an API call
  return new Promise((resolve) => {
    setTimeout(() => {
      resolve('Hello from Vuex store!');
    }, 1000);
  });
}

Implementing Code-Splitting and Lazy Loading

Code-splitting and lazy loading can significantly reduce the initial load time by loading only the necessary parts of your application first and deferring the rest until they are needed.

Code-splitting and lazy loading can significantly reduce the initial load time by loading only the necessary parts of your application first and deferring the rest until they are needed.

Dynamic Imports for Lazy Loading

Use dynamic imports to split your code and load components lazily.

router.js
import Vue from 'vue';
import Router from 'vue-router';

Vue.use(Router);

const Home = () => import('./components/Home.vue');
const About = () => import('./components/About.vue');

export function createRouter() {
  return new Router({
    mode: 'history',
    routes: [
      { path: '/', component: Home },
      { path: '/about', component: About }
    ]
  });
}

Managing State with Vuex and SSR

Proper state management is essential for SSR, as it ensures that the server-rendered HTML matches the client-side state when the page is hydrated.

Initializing State on the Server

Ensure that your Vuex store is correctly initialized on the server with the necessary state before rendering.

entry-server.js
import { createApp } from './main';

export default context => {
  return new Promise((resolve, reject) => {
    const { app, router, store } = createApp();

    router.push(context.url);

    router.onReady(() => {
      const matchedComponents = router.getMatchedComponents();
      if (!matchedComponents.length) {
        return reject({ code: 404 });
      }

      Promise.all(matchedComponents.map(Component => {
        if (Component.asyncData) {
          return Component.asyncData({ store, route: router.currentRoute });
        }
      })).then(() => {
        context.state = store.state;
        resolve(app);
      }).catch(reject);
    }, reject);
  });
};

Hydrating the Client-Side State

Ensure the client-side store is hydrated with the initial state provided by the server.

entry-client.js
import { createApp } from './main';

const { app, router, store } = createApp();

if (window.__INITIAL_STATE__) {
  store.replaceState(window.__INITIAL_STATE__);
}

router.onReady(() => {
  app.$mount('#app');
});

Implementing Advanced SEO Techniques

SEO is crucial for driving organic traffic to your site. Advanced SEO techniques can help you make the most out of SSR in Vue.js.

Structured Data and Schema Markup

Implement structured data using Schema.org markup to help search engines understand the content of your pages better.

Home.vue
export default {
  name: 'Home',
  metaInfo: {
    title: 'Home Page',
    meta: [
      { name: 'description', content: 'This is the home page of our Vue SSR app' },
      { property: 'og:title', content: 'Home Page' },
      { property: 'og:description', content: 'This is the home page of our Vue SSR app' }
    ],
    script: [
      {
        type: 'application/ld+json',
        json: {
          "@context": "https://schema.org",
          "@type": "WebPage",
          "name": "Home Page",
          "description": "This is the home page of our Vue SSR app"
        }
      }
    ]
  },
  asyncData({ store }) {
    return store.dispatch('fetchMessage');
  }
};

Optimizing for Accessibility

Ensuring your application is accessible to all users, including those with disabilities, is both a best practice and a legal requirement in many regions.

ARIA Attributes and Semantic HTML

Use ARIA attributes and semantic HTML to enhance the accessibility of your application.

Home.vue
<template>
  <div>
    <h1>{{ message }}</h1>
    <nav>
      <ul>
        <li><router-link to="/" aria-current="page">Home</router-link></li>
        <li><router-link to="/about">About</router-link></li>
      </ul>
    </nav>
  </div>
</template>

<script>
export default {
  name: 'Home',
  computed: {
    message() {
      return this.$store.state.message;
    }
  },
  asyncData({ store }) {
    return store.dispatch('fetchMessage');
  }
};
</script>

Monitoring and Improving Performance

Continuous monitoring and performance improvements are essential for maintaining a fast and responsive application.

Performance Monitoring Tools

Use tools like Google Lighthouse, WebPageTest, and SpeedCurve to continuously monitor your application’s performance and identify areas for improvement.

Regular Audits and Updates

Regularly audit your application for performance, security, and SEO issues. Keep your dependencies and libraries up to date to benefit from the latest features and improvements.

Conclusion

Implementing server-side rendering in Vue.js can significantly improve the initial load speed, SEO, and overall performance of your application. By setting up efficient builds, leveraging modern JavaScript features, managing state effectively, and continuously monitoring performance, you can ensure your application delivers a fast, engaging, and accessible user experience. Stay proactive with regular audits and updates to keep your application running smoothly and meeting the evolving needs of your users.

Read Next: