Prerender Vue.js Apps With prerenderer-webpack-plugin

Joshua Bemenderfer

SSR (Server-Side Rendering) gets all the love these days. Speeding up initial-page-loads by sending a full HTML page instead of a skeleton with a few scripts is a really great idea. There’s a catch though. SSR is hard. There are a lot of things to worry about and it breaks easily. The truth is, in most cases, SSR is overkill. You can gain most of the benefits of SSR in your Vue.js app by using prerenderer-webpack-plugin to pre-render your site to static pages at build time.

Pre-rendering is sort of like Server-Side Rendering mixed with static site generation, but simpler. You tell the pre-renderer which routes you want, then it fires up a browser or equivalent environment, loads the pages, and dumps the resulting HTML into file paths that match the routes. This gives you fully-rendered static pages, resulting in faster perceived load times and no need for odd hacks for deployment on static site servers. Once the JavaScript finishes loading, your app will continue to work as normal.

Pre-rendering isn’t a silver bullet though, don’t use it if you have hundreds of routes, or need to pre-render dynamic content without placeholders.

Anyway, enough chit-chat. Let’s see how to do it.

Note: prerenderer and prerenderer-webpack-plugin are still unstable, so API usage may change in the future. I'll try to keep this article up-to-date, but no promises. See the official documentation if something here doesn't work properly. If you want something more stable, (but with varying results) try prerender-spa-plugin.

This guide assumes you’ve set up a quick projet with vue-cli with the webpack-simple template, though it’s pretty much the same for any webpack setup.

Usage

Install prerenderer-webpack-plugin in your Vue.js project.

# Yarn
$ yarn add prerenderer-webpack-plugin -D
# or NPM
$ npm install prerenderer-webpack-plugin --save-dev

Then, in webpack.config.js, require() the relevant packages. prerenderer separates the actual renderer from the core, so don’t forget to add that too.

webpack.config.js (Partial)

var path = require('path')
var webpack = require('webpack')
// Add these
const PrerendererWebpackPlugin = require('prerenderer-webpack-plugin')
// Renders in your system browser by opening tabs, rendering your app, then closing tabs.
// See also: JSDOMRenderer, ChromeRenderer
const BrowserRenderer = PrerendererWebpackPlugin.BrowserRenderer

...

module.exports = {
  ...
  plugins: [
    new PrerendererWebpackPlugin({
      staticDir: __dirname, // The path to the folder where index.html is.
      routes: ['/'], // List of routes to prerender.
      renderer: new BrowserRenderer()
    })
  ]
}

...

Now, after you run webpack again, you should find that index.html in the project root now contains the rendered content of the page as well.

Switching Renderers

BrowserRenderer is great… except when it isn’t. It’s not super fast and can’t render a ton of pages without hogging a ton of system resources.

If you need to render hundreds or thousands of pages, JSDOMRenderer might be a better choice. jsdom fakes a browser environment inside of Node.js and mocks as much as it can. It does a fairly good job, but can’t handle everything. You may need to adjust your app to work around certain new or unusual APIs that might not be present.

JSDOMRenderer Example:

var path = require('path')
var webpack = require('webpack')
// Add these
const PrerendererWebpackPlugin = require('prerenderer-webpack-plugin')
const JSDOMRenderer = PrerendererWebpackPlugin.JSDOMRenderer

...

module.exports = {
  ...
  plugins: [
    new PrerendererWebpackPlugin({
      staticDir: __dirname, // The path to the folder where index.html is.
      routes: ['/'], // List of routes to prerender.
      renderer: new JSDOMRenderer()
    })
  ]
}

...

If you just want BrowserRenderer-level (or better) quality, you can also try ChromeRenderer which uses Chrome’s Remote Debugging Protocol under the hood to render your app in headless mode. The results are great and the speed is slightly better than BrowserRenderer. For more information, see the ChromeRenderer documentation.

If you wanted, it’s fairly trivial to implement your own renderer as well. See the official documentation for more information.

Configuration

Delayed rendering

You don’t always want to render the page right off. Sometimes you want to wait for something to happen.

All three renderers can can wait for three different triggers:

Wait for an element to exist:

renderer: new BrowserRenderer({
  // Wait to render until the element specified is detected with document.querySelector.
  renderAfterElementExists: '#app'
})

Wait for a document event to be fired:

You can trigger an event in your app with document.dispatchEvent(new Event('my-document-event'))

renderer: new BrowserRenderer({
  // Wait to render until a specified event is fired on the document.
  renderAfterDocumentEvent: 'my-document-event'
})

Wait for a specified amount of time:

renderer: new BrowserRenderer({
  // Renders after 5000 milliseconds. (5 seconds.)
  renderAfterTime: 5000
})

Variable injection

The renderers can also inject a variable into the global scope before your app scripts run. This is useful if you need to pass something to your app or change behavior depending on whether or not the page is being prerendered.

renderer: new BrowserRenderer({
  // injectProperty: '__PRERENDER_INJECTED'
  // You can change the property added to window. The default is window.__PRERENDER_INJECTED.
  // Inject can be anything that is JSON.stringify-able.
  inject: {
    nestLocation: 'bayou'
  }
})

Now in your app, window.__PRERENDER_INJECTED.nestLocation === 'bayou'

That's about it for now! Enjoy pre-rendering!

  Tweet It
✖ Clear

🕵 Search Results

🔎 Searching...