Before diving into SSR, let’s explore what it actually does for you and when you might need it.
Google and Bing can index synchronous JavaScript applications just fine. Synchronous being the key word there. If your app starts with a loading spinner, then fetches content via Ajax, the crawler will not wait for you to finish.
This means if you have content fetched asynchronously on pages where SEO is important, SSR might be necessary.
Users might come to your site from a remote area with slow Internet - or just with a bad cell connection. In these cases, you’ll want to minimize the number and size of requests necessary for users to see basic content.
You can use Webpack’s code splitting to avoid forcing users to download your entire application to view a single page, but it still won’t be as performant as downloading a single, pre-rendered HTML file.
For some demographics or areas of the world, using a computer from 1998 to access the Internet might be the only option. While Vue only works with IE9+, you may still want to deliver basic content to those on older browsers - or to hipster hackers using Lynx in the terminal.
If you’re only investigating SSR to improve the SEO of a handful of marketing pages (e.g. /
, /about
, /contact
, etc), then you probably want prerendering instead. Rather than using a web server to compile HTML on-the-fly, prerendering simply generates static HTML files for specific routes at build time. The advantage is setting up prerendering is much simpler and allows you to keep your frontend as a fully static site.
If you’re using Webpack, you can easily add prerendering with the prerender-spa-plugin. It’s been extensively tested with Vue apps - and in fact, the creator is a member of the Vue core team.
If you’ve gotten this far, you’re ready to see SSR in action. It sounds complex, but a simple node script demoing the feature requires only 3 steps:
// Step 1: Create a Vue instance var Vue = require('vue') var app = new Vue({ render: function (h) { return h('p', 'hello world') } }) // Step 2: Create a renderer var renderer = require('vue-server-renderer').createRenderer() // Step 3: Render the Vue instance to HTML renderer.renderToString(app, function (error, html) { if (error) throw error console.log(html) // => <p server-rendered="true">hello world</p> })
Not so scary, right? Of course, this example is much simpler than most applications. We don’t yet have to worry about:
In the rest of this guide, we’ll walk through how to work with some of these features. Once you understand the basics, we’ll then direct you to more detailed documentation and advanced examples to help you handle edge cases.
It’s kind of a stretch to call it “server-side rendering” when we don’t actually have a web server, so let’s fix that. We’ll build a very simple SSR app, using only ES5 and without any build step or Vue plugins.
We’ll start off with an app that just tells the user how many seconds they’ve been on the page:
new Vue({ template: '<div>You have been here for {{ counter }} seconds.</div>', data: { counter: 0 }, created: function () { var vm = this setInterval(function () { vm.counter += 1 }, 1000) } })
To adapt this for SSR, there are a few modifications we’ll have to make, so that it will work both in the browser and within node:
window
), so that we can mount it.Accomplishing this requires a little boilerplate:
// assets/app.js (function () { 'use strict' var createApp = function () { // --------------------- // BEGIN NORMAL APP CODE // --------------------- // Main Vue instance must be returned and have a root // node with the id "app", so that the client-side // version can take over once it loads. return new Vue({ template: '<div id="app">You have been here for {{ counter }} seconds.</div>', data: { counter: 0 }, created: function () { var vm = this setInterval(function () { vm.counter += 1 }, 1000) } }) // ------------------- // END NORMAL APP CODE // ------------------- } if (typeof module !== 'undefined' && module.exports) { module.exports = createApp } else { this.app = createApp() } }).call(this)
Now that we have our application code, let’s put together an index.html
file:
<!-- index.html --> <!DOCTYPE html> <html> <head> <title>My Vue App</title> <script src="/assets/vue.js"></script> </head> <body> <div id="app"></div> <script src="/assets/app.js"></script> <script>app.$mount('#app')</script> </body> </html>
As long as the referenced assets
directory contains the app.js
file we created earlier, as well as a vue.js
file with Vue, we should now have a working single-page application!
Then to get it working with server-side rendering, there’s just one more step - the web server:
// server.js 'use strict' var fs = require('fs') var path = require('path') // Define global Vue for server-side app.js global.Vue = require('vue') // Get the HTML layout var layout = fs.readFileSync('./index.html', 'utf8') // Create a renderer var renderer = require('vue-server-renderer').createRenderer() // Create an express server var express = require('express') var server = express() // Serve files from the assets directory server.use('/assets', express.static( path.resolve(__dirname, 'assets') )) // Handle all GET requests server.get('*', function (request, response) { // Render our Vue app to a string renderer.renderToString( // Create an app instance require('./assets/app')(), // Handle the rendered result function (error, html) { // If an error occurred while rendering... if (error) { // Log the error in the console console.error(error) // Tell the client something went wrong return response .status(500) .send('Server Error') } // Send the layout with the rendered app's HTML response.send(layout.replace('<div id="app"></div>', html)) } ) }) // Listen on port 5000 server.listen(5000, function (error) { if (error) throw error console.log('Server is running at localhost:5000') })
And that’s it! Here’s the full application, in case you’d like to clone it and experiment further. Once you have it running locally, you can confirm that server-side rendering really is working by right-clicking on the page and selecting View Page Source
(or similar). You should see this in the body:
<div id="app" server-rendered="true">You have been here for 0 seconds.</div>
instead of:
<div id="app"></div>
Vue also supports rendering to a stream, which is preferred for web servers that support streaming. This allows HTML to be written to the response as it’s generated, rather than all at once at the end. The result is requests are served faster, with no downsides!
To adapt our app from the previous section for streaming, we can simply replace the server.get('*', ...)
block with the following:
// Split the layout into two sections of HTML var layoutSections = layout.split('<div id="app"></div>') var preAppHTML = layoutSections[0] var postAppHTML = layoutSections[1] // Handle all GET requests server.get('*', function (request, response) { // Render our Vue app to a stream var stream = renderer.renderToStream(require('./assets/app')()) // Write the pre-app HTML to the response response.write(preAppHTML) // Whenever new chunks are rendered... stream.on('data', function (chunk) { // Write the chunk to the response response.write(chunk) }) // When all chunks are rendered... stream.on('end', function () { // Write the post-app HTML to the response response.end(postAppHTML) }) // If an error occurs while rendering... stream.on('error', function (error) { // Log the error in the console console.error(error) // Tell the client something went wrong return response .status(500) .send('Server Error') }) })
As you can see, it’s not much more complicated than the previous version, even if streams may be conceptually new to you. We just:
Vue’s SSR is very fast by default, but you can further improve performance by caching rendered components. This should be considered an advanced feature however, as caching the wrong components (or the right components with the wrong key) could lead to misrendering your app. Specifically:
You should not cache a component containing child components that rely on global state (e.g. from a vuex store). If you do, those child components (and in fact, the entire sub-tree) will be cached as well. Be especially wary with components that accept slots/children.
With that warning out of the way, here’s how you cache components.
First, you’ll need to provide your renderer with a cache object. Here’s a simple example using lru-cache:
var createRenderer = require('vue-server-renderer').createRenderer var lru = require('lru-cache') var renderer = createRenderer({ cache: lru(1000) })
That will cache up to 1000 unique renders. For other configurations that more closely align to memory usage, see the lru-cache options.
Then for components you want to cache, you must provide them with:
name
serverCacheKey
function, returning a unique key scoped to the componentFor example:
Vue.component({ name: 'list-item', template: '<li>{{ item.name }}</li>', props: ['item'], serverCacheKey: function (props) { return props.item.type + '::' + props.item.id } })
Any “pure” component can be safely cached - that is, any component that is guaranteed to generate the same HTML given the same props. Common examples of these include:
serverCacheKey
function can just return true
)By now, you should understand the fundamental concepts behind server-side rendering. However, as you introduce a build process, routing, and vuex, each introduces its own considerations.
To truly master server-side rendering in complex applications, we recommend a deep dive into the following resources:
Properly configuring all the discussed aspects of a production-ready server-rendered app can be a daunting task. Luckily, there is an excellent community project that aims to make all of this easier: Nuxt.js. Nuxt.js is a higher-level framework built on top of the Vue ecosystem which provides an extremely streamlined development experience for writing universal Vue applications. Better yet, you can even use it as a static site generator (with pages authored as single-file Vue components)! We highly recommend giving it a try.
© 2013–2017 Evan You, Vue.js contributors
Licensed under the MIT License.
https://vuejs.org/v2/guide/ssr.html