Nuxt: Making your website fast

Nuxt for fast websites

You are looking for short loading times and high interactivity for your website? Nuxt got you covered: A JavaScript framework that serves a pre-rendered page for a fast first meaningful paint and then transforms the page into a full interactive single page application.

Nuxt and Universal Web Applications

These kinds of websites are “universal” applications, as JavaScript code runs both on the server and on the client. If you don’t want to run JavaScript on the server in production, you can pre-render the pages and serve them via a content delivery network (CDN). This helps you to scale your website even better, so this is what I’m using here. Use Google’s Lighthouse to find out how your site performs.

Nuxt comes with support for Vue.js for components and Vuex for state handling. Webpack provides the build automation. If you are more inclined to use React, have a look at Next.js for a similar functionality.

To cover the basics, I recommend the Nuxt documentation for an introduction and a getting started. The following chapters assume that you are familiar with the essentials of npm, Nuxt and JavaScript. It requires no special knowledge about Vue.js. All configurations worked with Nuxt 2.3.4 and Chrome 71 (Update: they still work in Nuxt 2.8.0 and Chrome 74).

Making Nuxt faster

When you use Nuxt out of the box, it is already quite fast: A single HTTP request downloads the pre-rendered page with all CSS at the top, followed by non-blocking JavaScript and ready-to-display HTML. The browser renders it without the need to re-layout if you haven’t used external fonts and all images have a known size.

This post helps you to optimise this further by adding preview images and reducing the CSS included in the page. An upcoming post will cover how to optimise Nuxt for search engines and social media. I will show example code from this website (my homepage). I’ll introduce helpful plugins and their real-world use and configuration. Your requirements might be different, your mileage may vary depending on what you’re trying to achieve.

Extend Nuxt with webpack plugins

Showing pre-rendered HTML in the browser includes no images yet. The smaller the images are, the faster they will load. You can include preview images of a smaller resolution in the initial HTML while the user is waiting for the full image.

The following chapters show how you can add existing webpack plugins to extend Nuxt. You place all these plugins into the nuxt.config.js file. You don’t edit a webpack configuration file, but use JavaScript to change the webpack configuration.

Optimise SVG images with imagemin

Even if you use small vector SVG images, they contain unnecessary comments and tags you can remove. When you automate this in your build with the image-webpack-loader, the SVGs you serve are always optimal. The original files in your repository will remain unchanged.

First you add the new plugin and save it to your package.json file. As a development dependency it runs only at build time, and will not increase the size of your app in the browser.

Installing the image-webpack-loader
npm install --save-dev image-webpack-loader

Then add the plugin to the build process. The following snippet shows the lines you add to your configuration. Leave all other lines unchanged.

Adding the image-webpack-loader configuration to nuxt.config.js
// extending the webpack configuration
export default {
  build: {
    extend(config, {isDev, isClient}) {
      config.module.rules.forEach(rule => {
        if (String(rule.test) === String(/\.(png|jpe?g|gif|svg|webp)$/)) {
          // add a second loader when loading images
          rule.use.push({
            loader: 'image-webpack-loader',
            options: {
              svgo: {
                plugins: [
                  // use these settings for internet explorer for proper scalable SVGs
                  // https://css-tricks.com/scale-svg/
                  { removeViewBox: false },
                  { removeDimensions: true }
                ]
              }
            }
          })
        }
      })
    }
  }
}

The additional plugin configuration keeps the viewbox but removes the dimensions so Internet Explorer show the images in the correct size.

Optimise raster images with the responsive loader

Images in the correct size with optimal compression save bandwidth and speed up rendering your page, but this is tedious to ensure. The solution is the responsive loader plugin instead of the original image loader. Again, the original files remain untouched. Compared to minimising SVGs this takes more computing time during your build. It re-scales the images you use and also calculates previews. You can show the preview in the pre-rendered page while the browser loads the real image from the server.

Again, you first need to install the plugin. The plugin depends on an image scaler plugin. jimp is slower, but is a 100% JavaScript implementation that will run on any platform. If you have lots of images to transform, the plugin recommends using sharp for image scaling.

Installing the responsive-loader and sharp plugin
npm install --save-dev responsive-loader
npm install --save-dev sharp

Now you add the new loader to handle JPEG, GIF and PNG images, and tune the existing image loader to exclude them. For development you can skip generating previews and scaled images to speed up the build, but you cannot test the previews in development this way.

Adding the response-loader plugin to nuxt.config.js
export default {
  build: {
    extend(config, {isDev, isClient}) {
      // adding the new loader as the first in the list
      config.module.rules.unshift({
        test: /\.(png|jpe?g|gif)$/,
        use: {
          loader: 'responsive-loader',
          options: {
            // disable: isDev,
            placeholder: true,
            quality: 85,
            placeholderSize: 30,
            name: 'img/[name].[hash:hex:7].[width].[ext]',
            adapter: require('responsive-loader/sharp')
          }
        }
      })
      // remove old pattern from the older loader
      config.module.rules.forEach(value => {
        if (String(value.test) === String(/\.(png|jpe?g|gif|svg|webp)$/)) {
          // reduce to svg and webp, as other images are handled above
          value.test = /\.(svg|webp)$/
          // keep the configuration from image-webpack-loader here unchanged
        }
      })
    }
  },
}

It compresses the images using the quality stated in the configuration (here: 85%). This reduces the size of the images if your original files have a higher quality.

The new loader adds resizing functionality. When you reference images in your style sheets, you can add query parameters. The plugin creates a scaled image at build time and replaces this with the filename of the resized image.

Referencing image together with the size for rescaling in style file
.withbackground {
  background-image: url("/content/book.jpg?size=510");
}

You can also load images via JavaScript and use them in your templates. The src attribute will give you the standard URL. The placeholder attribute gives you a base64 data stream to embed in your page and show it to the user while she or he waits for the real image to arrive.

You can use this plugin to provide a scaled set of images as a source set. This allows the browser to select a pre-scaled image matching the screen resolution and displayed size of the image. While adding a source set is straightforward, you must also provide the sizes attribute with the expected size of the image depending on the screen’s width. If you don’t provide this attribute, the browser takes the full width of the window as the default and loads a bigger image than necessary. The following example sets the image widths via query parameters for this specific image. You can also provide defaults in the plugin’s configuration.

Loading an image inside JavaScript
const avatarbig = (await import(/* webpackMode: "eager" */'~/assets/jpg/alexander.jpg?sizes[]=400&sizes[]=800&sizes[]=1200&quality=95')).default
Using the image's preview in the template
<figure class="image is-1by1" :style="{backgroundSize: '100% 100%',
       backgroundImage: 'url(\'${avatarbig.placeholder}\')'}">
    <img alt="Photo of Alexander Schwartz" :src="avatarbig.src" sizes="400px" :srcset="avatarbig.srcSet" />
</figure>

The following box shows the two images side by side instead of layering them on top of each other. The preview is 1.1k (gzip’ed) and will be inlined in the pre-rendered page, the full picture is 30k and will be loaded afterwards.

Preview and real image comparison
Photo of Alexander Schwartz

Things to watch out for:

  1. Google Chrome will prefer a cached image with a higher resolution even if the sizes attribute indicates a smaller resolution. When you test and verify in the Network tab of the developer tools that the correct images are loading, you’ll need to issue a hard-reload or clear-cache-reload command. To do this, right-click on the reload button while your developer tools are open and choose the appropriate option.

  2. Google Chrome will also load the images even when the element is hidden. To not load the image, clear the background image when element is hidden, and use the picture element to direct images to a placeholder.

When hiding the element, also remove the background image
@media screen and (min-width: 567px) {
  .fixed {
    background-image: none;
    display: none
  }
}
Setting a 1x1 GIF image as a placeholder when image is not visible
<picture>
  <source srcset="..." media="(min-width: 568px)" sizes="255px">
  <source srcset="../../assets/png/1x1.png" media="(max-width: 568px)">
  <img src="..." />
</picture>

Optimise CSS with purgecss

In the default configuration each pre-rendered page of Nuxt contains all the CSS your application needs. You can change this and externalise it to a file that every pre-rendered page links to, but this would cause a re-layout of the page if the browser doesn’t have a cached version of the CSS.

Once the user navigates within your Nuxt application, this is no longer an issue as Vue.js will not reload the page but only exchange the information rendered in the DOM.

When you use a CSS framework, the first page sent to the browser can become quite big. The plugin purgecss helps you to include only the parts of the CSS framework you used on the site. This will substantially reduce the size.

Purgecss can run as a webpack plugin or a postcss plugin. For Nuxt you must use it as a postcss plugin to keep the CSS inside the page. There is now also a Nuxt plugin to do this, but as the lines of code necessary are very similar, I used the webpack plugin directly.

As a developer you can choose if you want to have purgecss running in development mode:

  1. When you make major CSS changes, you might need re-start the Nuxt development mode for purgecss to pick up the new CSS.

  2. If you disable purgecss in development mode, you might miss a situation where it removed some CSS in the production build you wanted to keep.

I’ve settled for the first option here. If you want to try the second option, you can install nuxt-purgecss that has an automatic development.

First install the plugin:

Installing the purgecsss plugin
npm install --save-dev @fullhuman/postcss-purgecss

This is my extended purgecss configuration. I’ve added additional content folders for purgecss to scan for CSS classes.

Adding the purgecss configuration to nuxt.config.js
export default {
  build: {
    postcss: {
      plugins: [
        purgecss({
          content: ['./pages/**/*.vue', './layouts/**/*.vue', './components/**/*.vue', './content/**/*.md', './content/**/*.json'],
          whitelist: ['html', 'body', 'has-navbar-fixed-top', 'nuxt-link-exact-active', 'nuxt-progress'],
          whitelistPatternsChildren: [/svg-inline--fa/, /__layout/, /__nuxt/],
        })
      ]
    },
  },
}

Purgecss needs some hints for CSS classes it can’t discover from the sources. It accepts these as a white list of CSS classes or as regular expressions. My white list includes the html and body tag that are present only in the generated index.html that’s not visible to purgecss. has-navbar-fixed-top is a CSS class I’ve set to the html tag using my Nuxt configuration using htmlAttrs (see below).

nuxt-progress is the progress bar when Nuxt is loading from the background, and nuxt-link-exact-active is the class added to Nuxt links when they are active. The layout and error pages use the __layout and __nuxt classes.

As I’m using font awesome, I add the svg-inline--fa classes it adds at run time.

Adding a CSS class to the root html tag in nuxt.config.js
export default {
  head: {
    htmlAttrs: {
      class: 'has-navbar-fixed-top',
      lang: 'en'
    }
  }
}

Summary

Thanks reading this post. You have seen how to

  • boost your user experience with preview images,

  • speed up your website by minimising images and CSS, and

  • extend Nuxt with webpack and postcss plugins.

You’ve looked at real-life configuration examples that will give you an idea for your own projects. If you have questions, contact me via a direct message on twitter or via email, I’m happy to help.

If you found this article helpful and you want to share it, please do so via the Twitter button below. If you found this article helpful and you want to share it, do so on Twitter.
Back to overview...