Production-ready Vue SSR in 5 Simple Steps

Oleg Pisklov
Namecheap Engineering
6 min readNov 20, 2019

I work at Namecheap as a Senior Software Engineer. In our company, we often use Vue.js on the frontend with server-side rendering (SSR). Setting up SSR in the first place, however, isn’t always so easy. That’s why I decided to describe this process in simple steps to make it easier for understanding.

Also, while reading the official documentation, I was thinking that it might be useful to see the full picture: how the application should look at the end. So I created a repo with an example.

In this article we’ll cover how to set up production-ready SSR for Vue application using:

  • Webpack 4
  • Babel 7
  • Node.js Express server
  • Webpack-dev-middleware and webpack-hot-middleware for comfortable dev environment
  • Vuex for state management
  • vue-meta plugin for metadata management

Let me note that we won’t cover the basics of these technologies. We’ll concentrate on SSR only, and jump right into the action. I hope you find it helpful… Now let’s get into it!

Step 1. Configure Webpack

At this point, you probably already have a Vue app, and if you don’t, feel free to use my repo as a boilerplate.

First, let’s take a look at our folders and files structure:

As you can see, it’s pretty standard except for a couple of things that might catch your eye:

  • there are two separate webpack configs for client and server builds: webpack.client.config.js and webpack.server.config.js.
  • there are two respective entry files: client-entry.js and server-entry.js.

This is actually a key configuration point of our application. Here is a great diagram from the official documentation that provides the architecture overview we’re implementing:

Client config is the one that you’ve probably already dealt with. It’s basically for building the application into plain JS and CSS files.

Server config is an interesting one. We need it to generate a special JSON file — server bundle, that will be used on the server side for rendering the plain HTML of the Vue app. We use vue-server-renderer/server-plugin for this purpose.

Another thing that is different from the client config is that we don’t need to process CSS files, so there are no loaders and plugins for it.

As you may have figured out, all common settings for client and server configs we put to the base config.

Step 2. Create Application Entries

Before we get into the client and server entries, let’s have a look at the app.js file:

Note that instead of just creating an app instance, we export a factory function createApp(). If our app were running in the browser env only, we wouldn’t have to worry about the users getting a fresh new Vue instance for each request. But since we’re creating the app in the node.js process, our code will be evaluated once and stay in the memory of the same context.

So if we use one Vue instance across multiple requests, it can lead to a situation when one user gets the app state of another’s. In order to avoid this scenario, we should create a new app instance for each request. Also, for the same reason, it’s not recommended that you use stateful singletons in the Vue app.

Every real-life app will have some metadata, like title or description, that should be different from page to page. You can achieve this with a vue-meta plugin. Click here to understand why we’re using ssrAppId option.

In the client entry, we call createApp(), passing the initial state injected by the server. After the router has completed the initial navigation, we mount the app to the DOM. Also in this file, you can import global styles and initialize directives or plugins that work with the DOM.

Server entry is pretty much described by the comments in the code. The one thing I’d add regarding the router.onReady() callback is that if we use a serverPrefetch() hook for data prefetching in some of our components, it waits until the promise returning from the hook is resolved. We’ll see an example of how to use it a bit later.

Now we can add scripts for building our app to the package.json:

Step 3. Run Express Server with Bundle Renderer

In order to render the app into plain HTML on the server side, we’ll use vue-server-renderer module and the ./dist/vue-ssr-server-bundle.json file that we generated by running build:server script. Let’s not think about development mode for now, we’ll discuss it in the next step.

First, we need to create a renderer by calling the createBundleRenderer() method and passing two arguments: the bundle that we generated earlier and the next options:

  • runInNewContext

Do you remember the problem with sharing the application state between multiple requests that we discussed in the previous step? This option aims to solve that. But creating a new V8 context and re-executing the bundle for each request is an expensive operation, so it’s recommended that you set this flag to false due to possible performance issues. Also, beware of using stateful singletons in the app.

  • template

There is a special comment, <! — vue-ssr-outlet — >, that will be replaced with HTML that’s generated by the renderer. And by the way, using the template option, the renderer will automatically add a script with declaring __INITIAL_STATE__ global variable that we use in client-entry.js to create our app.

Now, when we have a renderer instance, we can generate HTML by calling the renderToString() method, passing the initial state and current URL for the router.

Step 4. Set Up the Dev Environment

What do we need for a comfortable dev environment? I’d say the following:

  • run only one node.js server without using an additional webpack-dev-server
  • re-generate vue-ssr-server-bundle.json files every time our source code is changed
  • hot reloading

In order to accomplish all of these things, we can use the setupDevServer() function in server.js (see the previous step).

This function accepts two arguments:

  • app — the Express app;
  • onServerBundleReady() — callback that is called each time the source code is changed and new vue-ssr-server-bundle.json is generated. It takes the bundle as an argument.

In server.js we pass a callback onServerBundleReady() as an arrow function that accepts a fresh bundle and re-creates the renderer.

Note that we require all dependencies inside of the setupDevServer() function, we don’t need them to consume our process memory in production mode.

Now let’s add npm script for running the server in development mode using nodemon:

“dev”: “cross-env NODE_ENV=development nodemon ./server.js”,

Step 5. Use ServerPrefetch()

Most likely you’ll need to get some data from the server when your app is initializing. You can do it by simply calling API endpoint once a root component is mounted. But in this case, your user will have to observe some spinner — not the best user experience. Instead, we can fetch the data during SSR using the serverPrefetch() component hook that was added in 2.6.0 Vue version. Let’s add some endpoint to our server.

We’ll call this endpoint in getUsers action. Now let’s take a look at an example of using the serverPrefetch() hook in a component.

As you can see, we use serverPrefetch() along with a mounted() hook. We need it for cases when a user is sent to this page from another route on the client side, so the users array is empty and we call the API.

Also, check out how we define the title and the description metadata for a particular page in the metaInfo property provided by vue-meta plugin.

Well, this is it. I think we covered all the main configuration points of setting up SSR for Vue.js and I hope these steps helped you to better understand this process.

--

--