Speed up Nuxt Builds on Netlify

@lukeocodes ๐Ÿ•น๐Ÿ‘จโ€๐Ÿ’ป - Oct 25 '20 - - Dev Community

If you're not careful, your build times for Nuxt can spiral out of control. After enabling Nuxt i18n, we reached 27-minute production deploys.

Once we had added a load of nice-to-have Netlify plugins, our deploy times went up again. Before I knew it, our deploys on Netlify were failing, timing out at 30 minutes.

This post addresses build time problems for sites using version 2.14 of Nuxt. 2.14 introduces full-static builds and includes all the lovely new crawler changes.

Note: I will refer to the time it takes for Nuxt to run generate as build times. Deploy times are the time it takes Netlify to run generate and then publish it.

For this post, I'll be using the globally recognised "turd" scale for measuring satisfactionโ€“๐Ÿ’ฉ๐Ÿ’ฉ๐Ÿ’ฉ to ๐Ÿ˜๐ŸŽ‰๐Ÿ”ฅ.

๐Ÿ’ฉ๐Ÿ’ฉ๐Ÿ’ฉ 30-minute deploys (timeout)

Unnecessary Content

Unnecessary content was an issue for us. Having imported nearly 600 articles from our legacy WordPress site, we were building pages for every category, tag, and author too. This lead to some 17500 physical pages being rendered by the Nuxt full static build. After reviewing the metadata on our posts, I managed to reduce our build to just over 3000 pages...

Our Netlify build jumped from 30-minute timeouts to 15 minute deploys.

๐Ÿ‘Ž๐Ÿป๐Ÿ‘Ž๐Ÿป๐Ÿ‘Ž๐Ÿป 15-minute deploys

Screenshot of 15-minute deploy in Netlify

Skip Optional Dependencies

While not Nuxt specific, only installing the dependencies you need can speed up the Netlify install before the build even starts.

The --no-optional argument will prevent optional dependencies from being installed by npm. There is a yarn equivalent.

You can add this to Netlify in the config, or add it to the environment variables on the dashboard.

# netlify.toml

[build.environment] 
  NPM_FLAGS = "--no-optional"
Enter fullscreen mode Exit fullscreen mode

This did nothing for our deploy time, but it may help others. ๐Ÿคช

๐Ÿ‘Ž๐Ÿป๐Ÿ‘Ž๐Ÿป๐Ÿ‘Ž๐Ÿป 15-minute deploys

Code Minification

Nuxt has strong default HTML minification settings used for post-processing builds.

  // ...

  html: {
    minify: {
      collapseBooleanAttributes: true,
      decodeEntities: true,
      minifyCSS: true,
      minifyJS: true,
      processConditionalComments: true,
      removeEmptyAttributes: true,
      removeRedundantAttributes: true,
      trimCustomFragments: true,
      useShortDoctype: true
    }
  },

  // ...
Enter fullscreen mode Exit fullscreen mode

Nuxt already minifies CSS and JS using WebPack plugins. So we can disable inline CSS and JS minification.

  // ...

  build: {
    html: {
      minify: {
        minifyCSS: false,
        minifyJS: false,
      }
    }
  }

  // ...
Enter fullscreen mode Exit fullscreen mode

When I read about this, it suggested we'd see a 10x improvement on the initial build. In reality, we saw a minute or two in reductions.

๐Ÿ‘Ž๐Ÿป๐Ÿ‘Ž๐Ÿป๐Ÿ‘๐Ÿป 13-minute deploys

Turn Off Logging

Even locally, the verbose logging of several thousand lines in the terminal can slow EVERYTHING down. Most of the logging is formatted from Nuxt, to.

Disable logging anything but errors with the CI environment variable.

You can add this to Netlify in the config, or add it to the environment variables on the dashboard.

# netlify.toml

[build.environment]
  CI = "1"
Enter fullscreen mode Exit fullscreen mode

This made a surprising difference, slicing a fair chunk off our build time.

๐Ÿ‘Ž๐Ÿป๐Ÿ‘๐Ÿป๐Ÿ‘๐Ÿป 8-minute deploys

Post Processing

If you've played with Netlify deployment configurations, you'll know there is a wealth of options now.

All the Netlify deploy post-processing options

I had lots of options ticked, and we've already established that Nuxt does most of it already.

Reasons to turn them all ofโ€”for us at least:

  • We already have a plan to generate our images based on rules we've established for media creation already. We can do the optimisation steps and CDN uploads here in the future.
  • Nuxt does minification of HTML, JS and CSS already.
  • The Nuxt static build does-Pre-rendering.

Tick, tick, tick. All off.

๐Ÿ‘๐Ÿป๐Ÿ‘๐Ÿป๐Ÿ‘๐Ÿป 5-minute deploys

Screenshot of 5-minute deploy in Netlify

Real Talk

Getting our deploys much quicker without paying for enterprise Netlify is unrealistic. I have a ton of optimisation to do on my Vue components, but I don't expect to see much more time saved.

It wasn't actually the production build I was so keen to reduce the time on, it was the previews as we've gone FULL NETLIFY and adopted Netlify CMS for git-based content storage. So, every time we edit a post in Netlify CMS, it creates a pull-request for the edited file. At 30 minute builds, with a team of 8 people working on content, well you see where this is goingโ€”lots of waiting for builds. Less now, at least.

BUT WAIT, There's More ๐Ÿ˜ฎ

Is there a way to reduce our preview builds? That was the original motivation to reduce build times in the first place!

After a brief Googlin', I came across issue #6138 raised on the Nuxt project, on how to generate a single route.

In the latest version of Nuxt, the solutions in the GitHub issue didn't actually work.

But, it did give me an idea.

Since Nuxt 2.14, we've had the crawler to discover pages. And, if I want to provide additional routes, I can use the routes() property of the generator config.

So, I thought to myself, "can I turn off the crawler and provide it a single route, somehow"?

The answer was yes.

Casually breaking my nuxt.config.js...

  generate: {
    crawler: false,
    routes() {
      return ["/blog/a-test-blog-post-made-in-netlify-cms"]
    }
  }
Enter fullscreen mode Exit fullscreen mode

Doing this resulted in almost immediate build times, so once tested on Netlify I was down to about 1 minute deploys. It was just building the physical routes (everything in your /pages directory) without crawling for any dynamic routes. Our physical routes make up less of our site than was worth worrying about.

Could I make this context-driven based on the preview build?

Well, it hit me like a slap around the face.

The slug for the new post created in Netlify CMS was part of the branch name.

cms/blog/a-test-blog-post-made-in-netlify-cms

And, the branch name was available on our preview build as the environment variable, HEAD.

# console.log(process.env.HEAD)
cms/blog/a-test-blog-post-made-in-netlify-cms
Enter fullscreen mode Exit fullscreen mode
I'm getting closer.

A quick explore of the other environment variables provided in preview builds, I noticed PULL_REQUEST is an indicator whether the build is from a pull/merge request (true) or not (false).

So here was the rough code I put together to make use of this. Add a new function to the top of the nuxt.config.js file.

// nuxt.config.js

// ...

const isPreviewBuild = () => {
  return process.env.PULL_REQUEST && process.env.HEAD.startsWith('cms/')
}

// ...

module.exports = {
// etc...
Enter fullscreen mode Exit fullscreen mode

So this returns whether its a PR and if the branch name begins with cms/ (generated by Netlify CMS).

How can we use this? I'm glad you asked. Edit the "generate" property in nuxt.config.js.

// nuxt.config.js

// ...

module.exports = {
  // ...

  generate: {
    crawler: !isPreviewBuild(),
    routes() {
      return isPreviewBuild() ? ["/blog/a-test-blog-post-made-in-netlify-cms"] : []
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

Tested? Works, still my ~1 minute deploy! Now, route from the branch name. Another new function to nuxt.config.js.

// nuxt.config.js

// ...

const previewRoute = () => {
  const [, type, slug] = process.env.HEAD.split('/')

  return [ `/${type}/${slug}` ]
}

// ...

module.exports = {
// etc...
Enter fullscreen mode Exit fullscreen mode

The type is super important because it means we can also preview video and author pages as well blog. Really cool side effect. Added with one last edit to the nuxt.config.js file.

// nuxt.config.js

// ...

module.exports = {
  // ...

  generate: {
    crawler: !isPreviewBuild(),
    routes() {
      return isPreviewBuild() ? previewRoute() : []
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

๐Ÿ˜๐ŸŽ‰๐Ÿ”ฅ 1-minute deploys

Screenshot of 1-minute deploy in Netlify

Conclusion

Confession: I built most of the final code without testing it, I used the BRANCH environment variable thinking it would be the branch name. It is not. So when it didn't work, I thought I'd gone down a big old rabbit hole building this. I thought I'd wasted hours. Nope, got the environment variable wrong. Read the docs, folks.

It's been almost 5 years since I was in a role that had me building code that would be relied upon in a production environment. I shouldn't be surprised (but I am) that the off-the-shelf configuration for Nuxt isn't optimised for production builds.

I'm generally cautious of introducing environment-aware code into an application, but I have ignored my better judgement as this is environment-aware-configuration as code. You should be very careful about introducing code that fundamentally changes how an application runs or is built based on the environment it is running in.

  • Know your platform.
  • Read the flipping manual.
  • Do all the Googlin'.
  • Take care with environment-aware code.

See exactly how we're using this on our GitHub repository.

GitHub logo Nexmo / deved-platform

Nuxt.js for the new Vonage Developer Education site.

Credits

. . . . . . . . . . . . . . . . . . . . . . . . . . .