Performance checklist for Vue and Nuxt
Tags: vue, nuxt, performance, javascript
Published: 2021-10-11 14:47:56

Improving performance of web applications will always be sexy. We want the page to load faster, smoother, and without too many layout shifts (Core Web Vitals, I am looking at you 😉). If you search in Google for terms like `vue/nuxt performance` you will get a bunch of documentation and articles you can use to improve performance of your page. In this article I wanted to summarize all this knowledge into one single source of truth (with respect to article authors).

This summary document is based on the following articles:

* [My web performance journey with Nuxt, Storyblok & Netlify](https://www.dawntraoz.com/blog/my-web-performance-journey-with-nuxt-storyblok-netlify/) by @dawntraoz
* [How We Achieve 90+ Lighthouse Performance Score and Fully Offline Mode for DANA Home Shopping](https://medium.com/dana-engineering/how-we-achieve-90-lighthouse-performance-score-and-fully-offline-mode-for-dana-home-shopping-580b1b540c4d) by @jefrydco
* [Web Vitals, Google Search, the State Vue & Nuxt performance optimization in July 2020](https://medium.com/weekly-webtips/web-vitals-google-search-the-state-vue-nuxt-performance-optimization-in-july-2020-71441eefc51) by Johannes Lauter
* [Vue.js Performance](https://vueschool.io/articles/series/vue-js-performance/) by @filrakowski

and my own knowledge that I gathered throughout the years.

Make sure to visit these articles and give a solid like to all of them and their authors 😊

You can also check out other article I have written recently about continuously measuring the performance of Nuxt.js applications using Lighthouse CI and Github Actions [here](https://dev.to/baroshem/continuous-performance-checks-in-nuxt-js-with-lighthouse-ci-and-github-actions-2lj9)

<img width="100%" style="width:100%" src="https://media.giphy.com/media/3oz8xtBx06mcZWoNJm/giphy.gif?cid=ecf05e479t6z12wqipcr8vuqu4d0krvoymdaln178l8hfea2&rid=giphy.gif&ct=g">

Just please remember that improving performance is not an issue that you can just sit once and fix. It is a continuous process and the topic of performance should be addressed regularly so that new features of your website (for sure needed) won't break the performance.

## Preload key requests / Preconnect to required origins

Declare preload links in your HTML to instruct the browser to download key resources as soon as possible.

```html
<head>
  <link rel="preload" href="critical.css" as="style">
  <link rel="preload" href="critical.js" as="script">
</head>
```

Consider adding preconnect or dns-prefetch resource hints to establish early connections to important third-party origins.

```html
<link rel="preconnect" href="https://example.com">
<link rel="dns-prefetch" href="https://example.com">.
```

dns-prefetch works exactly the same as preconnect but has wider browser support.

## Reduce third-party usage

Third-party code can significantly impact load performance. You can however modify the way you are using this third party library by:

* Loading the script using the async or defer attribute to avoid blocking document parsing.
* Self-hosting the script if the third-party server is slow.
* Removing the script if it doesn't add clear value to your site.
* Use link rel=preconnect or link rel=dns-prefetch to perform a DNS lookup for domains hosting third-party scripts.

## Eliminate render blocking resources

Resources are blocking the first paint of your page. Consider delivering critical JS/CSS inline and deferring all non-critical JS/styles. You can reduce the size of your pages by only shipping the code and styles that you need.

Once you've identified critical code, move that code from the render-blocking URL to an inline script tag in your HTML page.

Inline critical styles required for the first paint inside a style block at the head of the HTML page and load the rest of the styles asynchronously using the preload link.

You can read more about this [here](https://sia.codes/posts/render-blocking-resources/#how-do-i-test-my-website-for-render-blocking-resources%3F)

## Minify/Remove unnecessary CSS and JS

When you are building a big application, you will get to a place where your project may have much more code that it actually needs and uses.

Use tools like [CSS Minification](https://web.dev/minify-css/#css-minification-with-webpack) or [Terser JS Plugin](https://github.com/terser/terser). In Nuxt, Terser is included by default.

To eliminate unused css use a tool like [PurgeCSS](https://purgecss.com/).

To eliminate unnecessary JavaScript you can use Terser mentioned previously or utilize [Tree Shaking](https://webpack.js.org/guides/tree-shaking/) to allow Dead Code Elimination. You can also use [Code Splitting](https://developer.mozilla.org/en-US/docs/Glossary/Code_splitting) which will split code into bundles that can be loaded on demand.

Nuxt provides code-splitting out of the box.

### Scan modules for duplicates

Remove large, duplicate JavaScript modules from bundles to reduce final bundle size.

![image](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/m8jgv7d98jpb0cwdz2gt.png)

Use [Webpack Bundle Analyzer](https://www.npmjs.com/package/webpack-bundle-analyzer) or `--analyze` flag in Nuxt.js

## Reduce execution time

The combination of code splitting, minification and compression, removal of unused code and caching techniques will greatly improve execution time.

Consider reducing the time spent parsing, compiling and executing JS. You may find delivering smaller JS payloads helps with this.
The idea is to optimize both our JS and CSS code, minimizing it and removing unused code, as well as the third-party libraries we are using.

Keep the server response time for the main document short because all other requests depend on it.

You can read more about this [here](https://gtmetrix.com/reduce-javascript-execution-time.html)

## Image handling

### Properly size images

Serve images that are appropriately-sized to save cellular data and improve load time.

```html
<img src="cat-large.jpg" srcset="cat-small.jpg 480w, cat-large.jpg 1080w" sizes="50vw">
```

You can read more about this [here](https://web.dev/uses-responsive-images/?utm_source=lighthouse&utm_medium=cli)

### Efficiently encode images

Optimized images load faster and consume less cellular data.
Using your image CDN service or the compression of your image should be enough.

You can read more about this [here](https://web.dev/uses-optimized-images/)

### Serve images in next-gen formats

Image formats like WebP or Avif often provide better compression than PNG or JPEG, which means faster downloads and less data consumption.

You can read more about this [here](https://web.dev/uses-webp-images/)

### Image elements have explicit width and height

Set an explicit width and height on image elements to reduce layout shifts and improve CLS.

You can read more about this [here](https://gtmetrix.com/use-explicit-width-and-height-on-image-elements.html)

### Preload largest contentful paint (LCP)

Preload the image used by the LCP element in order to improve your LCP time.

```html
<link rel="preload" href="/path/to/image.jpg" as="image">
```

```js
head() {
 return {
    link: [
      {
        rel: 'preload',
        as: 'image',
        href: 'path/to/lcp/image',
      },
    ],
  }
}
```

You can read more about this [here](https://gtmetrix.com/preload-largest-contentful-paint-image.html#:~:text=Preloading%20the%20Largest%20Contentful%20Paint,sooner%2C%20enhancing%20their%20user%20experience.)

## Fonts

### All text remains visible during webfont loads

Leverage the font-display CSS feature to ensure text is user-visible while webfonts are loading.

```css
@font-face {
  font-family: 'Arial';
  font-display: swap;
}
```

The font-display API specifies how a font is displayed. swap tells the browser that text using the font should be displayed immediately using a system font. Once the custom font is ready, it replaces the system font.

For Google fonts, for example, is as simple as adding the &display=swap parameter to the end to the Google Fonts URL:

```html
<link href="https://fonts.googleapis.com/css?family=Roboto:400,700&**display=swap**" rel="stylesheet">
```

You can read more about this [here](https://web.dev/font-display/)

## What to avoid?

### Large layout shifts

Cumulative Layout Shift (CLS) is a Core Web Vitals metric calculated by summing all layout shifts that aren’t caused by user interaction.

### Avoid an excessive DOM size

A large DOM will increase memory usage, cause longer style calculations, and produce costly layout reflows.

### Multiple page redirects

Redirects introduce additional delays before the page can be loaded.

### Serving legacy JavaScript to modern browsers

Polyfills and transforms enable legacy browsers to use new JavaScript features. However, many aren't necessary for modern browsers.

In Nuxt we have --modern with some options in the build command.

### Enormous network payloads

Large network payloads cost users real money and are highly correlated with long load times.

* Defer requests until they're needed. Nuxt is taking care of it.
* Optimize requests to be as small as possible, minimizing and compressing, try to use WebP for the images when it's possible. An image CDN will be always there to keep our performance up!
* Cache requests so the page doesn't re-download the resources on repeat visits.

### Document.write()

For users on slow connections, external scripts dynamically injected via document.write() can delay page load by tens of seconds.

### Non-compositioned animations

Animations which are not composited can be heavy and increase CLS. Use `translate` and `scale` CSS properties instead.

## Framework improvements

We went through things that you can do with your HTML, CSS, and JavaScript. Now, let's tackle the framework layer to see what we can do to improve performance of our website.

### Asynchronous Components

Asynchronous Components allow you to only load Components when a specific condition is matched.

```vue
<template>
  <header>
    <Search v-if="searchActive" />
    <button @click="searchActive = !searchActive">
      🔍   
    </button>
  </header>
</template>
<script>
export default {
  components: {
    Search: () => import('~/components/search.vue')
  },
  data() {
    return {
      searchActive: false
    }
  }
}
</script>
```

### Route based code splitting

Only the code from route that is currently visited by the user will be downloaded.

So instead:

```js
// router.js
import Home from './Home.vue'
import About from './About.vue'

const routes = [
  { path: '/', component: Home }
  { path: '/about', component: About }
]
```

We could write this:

```js
// router.js 
const routes = [
  { path: '/', component: () => import('./Home.vue') }
  { path: '/about', component: () => import('./About.vue') }
]
```

If you’re using Nuxt this is out of the box. Nuxt’s default directory-based routing system is code-splitting every route by default.

### Use reactivity when it is actually needed

Overloading your page with too many reactive properties will make your page slower (especially using Vue 2). Make sure to use them only when needed and other static values that won't be changed over time, store them in constant variables.

So instead:

```js
export default {
  data() {
    return {
      milisecondsInAnHour: 3600000,
    }
  },
  computed: {
    getMilisecondsInAnHour() {
       return this.milisecondsInAnHour
    }
  }
}
```

Write something like this:

```js
const MILISECONDS_IN_AN_HOUR = 3600000;

export default {
computed: {
    getMilisecondsInAnHour() {
       return MILISECONDS_IN_AN_HOUR
    }
  }
}
```

### Eliminate memory leaks

The easiest example of a memory leak is registering an event listener and not properly unregistering it.
```js
export default {
  created() {
     target.addEventListener(type, listener);
  }
}
```

To avoid that, make sure to include removeEventListener on destroy lifecycle hook.

### Optimize third party packages

Many popular third party packages provide lighter versions that you can check using https://bundlephobia.com/. Bundlephobia helps you find the performance impact of npm packages. Find the size of any javascript package and its effect on your frontend bundle.

Make sure to use libraries that support tree shaking to only load code that will be used in the final configuration.

Some libraries like lodash support importing direct files instead of the whole library. So instead writing this:

```js
import { isNull } from 'lodash'
```

We can use this:

```js
import isNull from 'lodash/isNull`
```

### [Nuxt] Use plugins only if they are used app-wide

Plugins are a great way to provide application wide logic, but that also means that they are loaded application wide. If it turns out to be a piece of logic you’ll only need in certain conditions or certain pages consider loading it via dynamic import at these places.

### [Infrastructure] Use a Content Delivery Network (CDN)

A CDN allows for the quick transfer of assets needed for loading Internet content including HTML pages, javascript files, stylesheets, images, and videos.
The `build.publicPath` option allows you to configure a CDN for all assets.

## Useful Vue & Nuxt Packages

In terms of improving performance of your website there are several packages available you can use.

### Implement Progressive Web App

[@nuxjs/pwa package](https://pwa.nuxtjs.org/)

PWA will cache all of the resources needed to load our app. Not only the static files like JS and CSS, but it also caches the images. Even the API response is cached as well.

```js
pwa: {
  manifest: {
    name: 'My Awesome App',
    lang: 'fa',
  }
}
```

### Preconnect fonts

[@nuxtjs/google-fonts package](https://google-fonts.nuxtjs.org/)

If you are using Google Fonts like Roboto, Raleway, etc, you can use this package to not block the page from rendering.

```js
googleFonts: {
  families: {
    Roboto: true,
    Raleway: {
      wght: [100, 400],
      ital: [100]
    },
  }
}
```

### Use Optimized Images

[@nuxtjs/image package](https://image.nuxtjs.org/)

Make sure all images have right sizes and/or use external Digital Asset Management like Cloudinary to optimize images on the fly.

```js
  image: {
    cloudinary: {
      baseURL: 'https://res.cloudinary.com/<your-cloud-name>/image/upload/'
    }
  }
```

### Purge Unnecessary CSS

[nuxt-purgecss package](https://purgecss.com/guides/nuxt.html#nuxt-js-plugin)

PurgeCSS analyzes your content and your CSS files. Then it matches the selectors used in your files with the one in your content files. It removes unused selectors from your CSS, resulting in smaller CSS files.

### Lazy Hydration

[vue-lazy-hydration package](https://github.com/maoberlehner/vue-lazy-hydration)

The idea of lazy hydration is to be able to control what components are hydrated (with JavaScript), when and under what conditions.

```html
<LazyHydrate when-visible>
  <AdSlider/>
</LazyHydrate>
```

### Image Lazy Loading

[vue-lazyload package](https://www.npmjs.com/package/vue-lazyload)

The idea of lazy loading images is to delay sending requests for images to the point the image appears in the viewport. So basically, if you have an image in the footer, the request for it will be done when user scrolls down to it.

```html
<img v-lazy="img.src">
```

### Infinite Loading

[vue-infinite-loading package](https://peachscript.github.io/vue-infinite-loading)

The idea of the infinite load is as the user scrolling through the page goes on, we load the next paginated data.

```vue
<template>
  <infinite-loading></infinite-loading>
</template>

<script>
import InfiniteLoading from 'vue-infinite-loading';

export default {
  components: {
    InfiniteLoading,
  },
};
</script>
```

### Use Compression (Brotli)

[nuxt-compress package](https://www.npmjs.com/package/nuxt-compress)

Adding Brotli compression will reduce the overall file size of your application by a relevant margin, Alexander Lichter wrote a [great article](https://blog.lichter.io/posts/nuxtjs-on-brotli/) about how to add it.

```js
module.exports = {
  modules: [
    ['nuxt-compress', {
      brotli: {
        threshold: 8192,
      },
    }],
  ],
};
```

### Caching

[nuxt-ssr-cache package](https://www.npmjs.com/package/nuxt-ssr-cache)

Cache is a hardware or software component that stores data so that future requests for that data can be served faster.

```js
  cache: {
    useHostPrefix: false,
    pages: [
      '/page1',
      '/page2',
    ],
    store: {
      type: 'memory',
      max: 100,
      ttl: 60,
    },
  },
```

### Bonus

* Make sure to include `min-height` for all your main pages. We encountered this problem in Vue Storefront, that we were fetching the data from an Ecommerce platform and because of that it took some time for the content to arrive and it was causing layout shifts (which resulted in worse Lighthouse results that were relatively easy to fix).
* Make sure to scan your components for unnecessary imports. When developing a huge application, it is common to refactor your pages so that you are constantly adding or removing components. It is really easy to lose track of used and unused components/helpers/etc.
* Check your build configuration in `nuxt.config.js`. To decrease the final bundle you can add `extractCSS: true` to your `build` object. In our case it resulted in decreasing the final bundle by about 60 KB (20% of overall size).