Web Vitals, Google Поиск, состояние Vue и оптимизация производительности Nuxt в июле 2020

As google recently announced their new guidelines for Web Vitals as an update of their existing Lighthouse KPIs, a lot of discussion on how to to optimize against these values has come up. I wanted to document some of the best practices that I ended up using.

As a disclaimer, if your goal is to reach a performance score ≥ 85 with a tool like web.dev or pagespeed insights, any kind of client side hydration or JS in general is not your friend and using Nuxt with isomorphic rendering can only be optimized so far, especially if you have a somewhat complex application / site.

During the Google IO/19 Zoe & Martin also mentioned that Google Search ranks any page on the base of a first time visit, so features like client side navigation, pre-fetching, client caches & web workers, while great for the user, will have no direct beneficial impact on your ranking.

The main argument here is that they improve the UX to such a degree that it will impact the rankings trough a lower number of people returning to the SERPs after visiting the page - this is only true for a small number of pages tough.


Web Vitals

I wont go to much in to the new set of KPIs as google does pretty good Job of that themselves.

It basically breaks down in multiple measurements that indicate when and how fast a user can Interact with a page.

On top it also added some new recommendations for page improvements, like the notorious “Remove unused JavaScript”

Remove unused JavaScript

As of right now these KPIs & recommendations are not yet rolled out in Chrome Stable (build 83.0.4103.106) — but can be seen via googles web testing tools and newer builds — like canary.

Performance measurement

Baseline

Different frameworks will have a different performance baseline, you can get a good overview about the state of different frameworks at https://perf-track.web.app/

baseline

It uses the Chrome User Experience Report, so it is based on real live data. Vue by itself is doing pretty well, that changes if you start to compare Vue Nuxt vs. React Next.

But as the site says “Correlation does not imply causation.”.

Development

Of course your development performance indicators will be highly unstable and vary from what the production values will be — but they can still be helpful to spot larger mistakes or indicate changes.

Basic Browser Tools

lighthouse result set

To get a detailed report inside you can use the built in chrome lighthouse tools.

ff

For an instant overview of the web core vitals you can use the new web vitals chrome extension.

Performance deep dives

That being said the most powerful tool at hand you can use is analyzing the runtime Performance via the provided tools in chrome

Starting with it can be a bit daunting but it’s definitely worth the effort to go a trough the documentation.

Without much effort it will allow you to have an eye on:

  • animations during initial rendering ( does this notification really need to fade in — blocking the rendering process? )
  • number of active listeners
  • critical path of loaded assets and scripts

and much more. As a general rule it is recommended to start analyzing the runtime performance early in development, maybe working with a performance budget, to catch issues early.

Performance Throttling

Keep in mind that, especially as a developer, your machine is probably more powerful then most consumer devices, not to mention the average smartphone.

So if you want to get a realistic Benchmark, and not just monitor trends, definitely take a look at CPU & connection speed throttling.

nuxt dev vs. nuxt build & nuxt run

Last but not least make sure to test improvements not just in development but also with an actual build application — this might sound trivial but I’ve seen developers give up on approaches that would have worked because they didn’t see any difference in development.

As a site note, applying a GitHub flow based process here helps immensely as you’ll have feature based environments that match your production system much closer.

Production

Search Console

the new web vitals report in google search console

The easiest way to keep an eye on the indicators for your whole site is the new Web Vitals report in the google search console. Overall it seems like this report is more forgiving then what google generates when using pagespeed insights or web.dev reports — that is probably due to it being based on real world data and end users having better performing devices & connections then what google bases their own tests on.

Web Services

web.dev provides an overview of performance and a number of other characteristics, like accessibility & best practice — although it is very unclear what the ultimate impact of those will be on your ranking. As well as a basic History of measurements if you re logged in.

PageSpeed Insights focuses solely on, as the name suggests, PageSpeed … insights — it is the direct implementation of the Lighthouse library enriched with some publicly gathered data.

Last but not least a bit of a classic, webpagetest has been around forever but is still very much up to date and provides you with a bit of a hybrid of a KPI & Waterfall based Result that can also easily be shared.

Continuous Integration

If you want to keep track of your KPIs without the hassle of manual testing you can also take a look at implementing the lighthouse CLI in to your CI process.

Its not something I have done myself, so if you have some experience with it, feedback in the comments would be appreciated.


Improving Performance

I will try to break down possible points of optimization in to different categories and not to repeat the usual points that you will find for most search results but to focus on things that are usually not pointed out.

CSS

Choice of CSS Framework

There is a huge choice of UI & CSS frameworks to choose from when using Vue or Nuxt specifically with their own advantage and disadvantages.

There are in principle two kind of UI Systems you can use, ether one that ships with pre-existing Vue Components, their styles & functionality, like Vuetify and Quasar or pure CSS frameworks that you will use to build your own component library like Tailwind or Bulma.

Which one of these is best is a matter of opinion and personal taste, but generally speaking, systems that ship with their own Vue logic will be harder to optimize and have their own ceilings.

Especially the Vuetify team is working hard on improving the performance of the system, but as they have to support a larger feature scope and use cases that you would by yourself — there is only so much they can do.

When optimizing for performance, and assuming the required development resources are available, my best experience so far has been with tailwind, as it allows you to reduce the loaded CSS to a minimum and create highly optimized & specialized UI components and layouts.

CSS Modules

We all like a nice BEM CSS structure, what we don’t like is ending up with KBs off class names in our HTML

<span class="ui-button ui-button--success ui-button--flat">
  click me
</span>

instead of something like

<span class="_iud7 _81-z _ztwc">
  click me
</span>

Enabling CSS Modules will help you shorten & obfuscate these declarations, enable it by adding the loader to your nuxt.config.js and use the appropriate syntax in your component:

build: {
  loaders: {
    cssModules: {
      modules: {
        localIdentName: '[hash:base64:4]'
      }
    }
  }
}

Component:

<template>
  <div     
    :class="[
      success ? $style['ui-button--success'] : '',
      flat ? $style['ui-button--flat'] : '',
      $style['ui-button']
    ]"
  >
    <slot />
  </div>
</template>
<script>
  export default {
    // logic and prop definition for flat and success here
  }
</script>
<style module lang="sass">
.ui-button
  // styles here
  &--success
  // styles here
  &--flat
  // styles here
</style>

I decided to with sass here as well, that is not required, I just prefer it.

If you are where using classes es selectors for end 2 end tests with a tool like cypress this will break them, but you can simply use a custom data property which you remove on production builds in your nuxt.config.js

build: {
  extend(config, ctx) {
    if ('production' === process.env.NODE_ENV) {
      const tagAttributesForTesting = ['data-e2e']
      ctx.loaders.vue.compilerOptions = {
        modules: [{
          preTransformNode(astEl) {
            const {
              attrsMap,
              attrsList
            } = astEl
            tagAttributesForTesting.forEach((attribute) => {
              if (attrsMap[attribute]) {
                delete attrsMap[attribute]
                const index = attrsList.findIndex(x => x.name === attribute)
                attrsList.splice(index, 1)
              }
            })
            return astEl
          }
        }]
      }
    }
  }
}

PurgeCSS

PurgeCSS is a tool to remove, or purge, unused CSS — this reduces the size of the page quite a lot and takes a lot of work off your hand when you are using a utility based framework. There are a Number of Integrations to use PurgeCSS with Vue and Articles about it

So I won’t get in to it to much here.

The main thing to be aware of is that PurgeCSS only “sees” selectors that are directly used. So generated selectors need to be whitelisted. When using BEM simple regex patterns can save you a lot of lines

whitelistPatterns: [
   /ui-button--(green|gray|accent|ghost|white)$/
]

Vue & JavaScript

Right now one of the biggest, if not THE biggest, Issue with isomorphic applications is the initial view.

Instead of getting the best of both worlds ( server & client side rendering ), you are actually just getting both wholly, first the site is rendered on the server, delivered to the user, and then rendered again — seems kind of waste full, doesn’t it?

Unfortunately Google seems to agree

alt

What that basically translates to is google telling you that you are loading to much JavaScript, and that its doing to much. So what you would want to do is, load less, and do fewer things. How does that translate to actual development concepts?

only load what you need and execute when you need

There are some tools & methodologies that can help you get closer to that result.

Asynchronous Components

Asynchronous Components allow you to only load Components when a specific condition is matched, this is helpful with basically everything that requires a user interaction before it should be loaded.

For example you can avoid loading all your complex search logic & visuals till the user actually indicates that he wants to search for something:

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

Component Lazy Hydration

This solves the issue of code that wont be immediately required, but still leaves the problem of loading and executing code that is required for the initial view but that actually does not provide any benefit by being executed on the client site again.

A simple example here would be markdown text that is parsed to HTML — this does not need to be re-executed by the client.

Thankfully there are integrations that allow lazy component hydration like

<!-- Article is already rendered - no hydration needed -->
<LazyHydrate ssr-only>
  <ArticleContent :content="article.content"/>
</LazyHydrate>
<!-- hydrate adds when visible -->
<LazyHydrate when-visible>
  <AdSlider/>
</LazyHydrate>
<!-- `on-interaction` listens for a `focus` event by default ... --><LazyHydrate on-interaction>
  <CommentForm :article-id="article.id"/>
</LazyHydrate>

All credit and my personal thanks to the author Markus Oberlehner 🙇‍♂

What it basically does is to allow you not hydrate specific components on client side or only hydrate them in certain conditions. The Documentation describes its usage very well — so I won’t go to much in it but just go trough some pitfalls I found.

  • if you are optimizing for web vitals avoid the hydrateWhenIdle logic as it does not seem to have any beneficial effect
  • there are some cases where elements need to be wrapped with another element when you want to use hydrateWhenVisible
  • the lazy components only effect the loading of components, not the execution of any other logic inside
  • using fetch on view component level will allow you to further reduce the amount of code executed in your page components

Unfortunately lazy hydration only stops the execution of the code, but its still loaded, there exists a PR to fix this on Vue level, unfortunately due to the focus on Vue 3, it has not been merged yet.

There is a workaround for this to not pre-fetch this code, again unfortunately this workaround stops working with Nuxt 2.12.2 as long as a http2.push is enabled and stops working with 2.12.3 all together.

Asynchronous Code / Dynamic Imports

Just like components, any JS code can be loaded via dynamic imports which provide a powerful tool to load most logic you’ll need just in time or conditionally — reducing overhead

<script type="module">
  const moduleSpecifier = './utils.mjs';
  import(moduleSpecifier)
    .then((module) => {
      module.default();
      // → logs 'Hi from the default export!'
      module.doStuff();
      // → logs 'Doing stuff…'
    });
</script>

Image Lazy Loading

There are many (like… so many) lazy loading components for images.

Keep an eye on their size and complexity ( having a large & complex library kind of defeats purpose here) that uses the Intersection Observation API without bundling a polyfill.

I’ve made good experience loading polyfills with https://polyfill.io/v3/ as you’ll only load the code for the low number of users actually needing them instead of bundling them with your code.

Overhead

bundle size & external libraries

The webpack bundle analyzer provides a great tool to get a handle on the size of your actual codebase, the one shipped to the user.

Nuxt already ships with an integration (lucky us!)

yarn nuxt build --analyze

Jennifer Bland did a good write up about how to use it last year which is still pretty accurate.

I will go trough some of my own experiences with it with a regular Nuxt application to point out some common pattern you might come across

webpack bundle analyzer of a normal nuxt app

You immediately see the breakdown in to the different bundles your application is gonna use, this is gonna get larger as your application grows. Ideally you should aim for keeping your common.app, app & vendor bundles small.

Two things immediately stick out here, the number of Lodash files loaded & the size of Fontawesome.

The number of Lodash code is the result of 2 functions used:

import isUndefined from 'lodash/isUndefined'
import forEach from 'lodash/forEach'
export default {
  methods: {
    isUndefined,
    forEach
  }
}

While Fontawesome doesn’t even load any Icons yet in the application. Definitely two points worth considering when you want to optimize your code size for production as this seems out of proportion.

Plugins

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.

Nuxt

Nuxt actively works on a lot of performance issues, you can get an up to date of their initiatives and recommendations here.

It also ships with a pretty good standard config, so there aren’t that many things that you can archive on a pure config level, but there are some.

Modern/Module

Javascript Modules can be used to load module JS code for modern browsers and legacy code for older browser.

This allows for slightly optimized code for modern browsers, Nuxt provides a simple implementation for it:

Two things to keep in mind

  • lighthouse does not seem to use modules yet
  • when running webpack analyze add the modern property as well
yarn nuxt build --modern --analyze

Ressource Hints

Nuxt automatically adds resource hints to the page, pre-loading code for other pages which can be disabled.

This feature can quickly get out of hands for larger sites and generally provides no benefit for the initial page load.

Compression / Brotli

Adding Brotli compression support will reduce the overall file size of your application by a relevant margin, Alexander Lichter wrote a great article about how to add it.

Infrastructure

This is more general and does only relate to Nuxt / VUE as it relates to all web technologies used to generate websites,

Use a CDN

The build.publicPath option allows you to configure a CDN for all assets. There are a lot of possible providers all with their own pitfalls and benefits, some tings you should keep an eye on:

  • supported protocols (H2, H3)
  • supported compression (for example Cloudfront needs you to enable certain headers to enable Brotli compression)
  • pricing
  • locations

Be aware that build.publicPath is used by some extensions and plugins internally to generate Urls, so check those after setting up the configuration.

One example is nuxt/sitemap till version 3 (there is a workaround tough).

HTTP/2 & HTTP/3

This is probably one of the more complicated issues as it can heavily rely on your host, for example as google is rolling out HTTP/3 while Heroku isn’t even supporting HTTP/2.

There are a lot of Benefits when using HTTP/2 like

  • Compression of request headers
  • Binary protocol
  • HTTP/2 Server Push
  • Request multiplexing over a single TCP connection
  • Request pipelining
  • HOL blocking (Head-of-line) — Package blocking

And more with HTTP3, the main focus here is to switch from TCP to UDP to allows better handling of interruptions like package loss. It also adds support for 0-RTT Handshakes, eliminating TLS acknowledgement for subsequent requests, speeding them up.

Full Response Caching

I would like to say that full response caches (caching the full HTML response for the initial server side request ) are a great way to optimize your page, but as things stand right now its basically a pre-requisite.

Nuxt provides an ssr cache module which will cover your basic response caching needs.

If you have the opportunity i would recommend in distinct response caching layer tough for the following reasons

avoid the dogpile effect

After an entry is expired multiple requests could request & generate a new entry overloading your system.

implement stale-white-revalidate

This pattern allows you to deliver a stale entry to a user while a new one is generated, enabling you to always deliver a cached/fast response to the user within a defined time.

separation of concern

running your cache & content system together means that will also crash & go down together, also making it harder do optimize ether.

what to choose

Adding a basic distinct caching layer can be done by using a simple CDN like Cloudfront or or more advanced like Cloudflare.

In my personal experience i would recommend a VCL based tool like Varnish or Fastly as the programmatic approach gives you the best control over how your caching is handled.

It also gives you an entry point for every day system operation tasks like redirects, forwarding, handling of specific requests that you would not like to add to your webapp.

The following statements & ideas are mostly my own opinions and should thus be taking with a grain of salt as they partially collide with what people (including myself) might consider best practice.

Miscellaneous

Over Abstraction

Avoid over abstraction and long dependency chains, separation of concern is a great practice but can be a bit of a pitfall for developers that are used to write code that is only executed on a server, it can easily lead to implementation chains like this:

ArticlesRow > ArticleList > AricleTeaser + UiRow + UiCol > UiTeaser > UiCard > UiSheet + UiButton + UiImage

So think of it as “separation of relevant concern”

  • does this abstraction actually provide any benefit or does it just add additional layers?
  • will it actually make changing code easier, or will I just have to touch 4 files instead of one implementation?
  • does the increased code quality match with the performance budget?

CSS Components

Layout / CSS components are great for providing a programmatic interface for the structuring & styling of your page, that can apply to: headings, grids, containers, columns etc.

Lets take a look at Vuetify which provides v-container, v-row & v-col, pretty neat from a DX standpoint, but I am sure you can imagine how much more work for the browser that is with the average number of those elements on every page.

So, if your components just add/toggle CSS classes, just use CSS classes in the first place — sorry.

What else can you do?

If you follow all these practices while implementing common best practices you’ll probably end up with a reasonably fast website — if your goal is to optimize above a 90 performance mobile score tough this might not be enough.

So there are really only 3 options left to you

Wait for nuxt & vue to implement optimizations

There are exciting things coming down the pipeline for Vue 3 like

  • global API tree-shaking
  • reduced bundle size
  • proxy-based reactivity
  • time slicing
  • optimized output code
  • unnecessary parent / children re-renders will be avoided
  • better component design trough component API

source: https://vueschool.io/articles/vuejs-tutorials/faster-web-applications-with-vue-3/

and nuxt itself

  • Webpack 5 support
  • Reducing initial bundle size
  • Vue3 Support
  • Select features individually to reduce bundle sizes (experimental)
  • Smart SSR Module (stale-while-revalidate cache aka SPR and dynamic SSR/SPA switching)
  • Multi target builds (static, server, serverless)
  • Full static support
  • Lamba API Support
  • CI/CD for continues performance measurements (for PRs to the core)
  • Critical CSS extraction
  • Image Module (lazy-loading and size-optimize)
  • Supporting APP Shell (SSR only for layout)

source: https://github.com/nuxt/nuxt.js/issues/6467

But it is hard to say when this will ultimately available and what the real world impact of them will be.

Switch to AMP for mobile

Nuxt provides an AMP implementation, so if your main concern is performance for mobile visitors via Google Search this might be an option for you.

And if you are having success with it, maybe you can help them extend the documentation a bit ;)

Drop client side JS

The nuclear option is to completely drop the Nuxt generated client side scripts and deliver a minimal client side JS bundle. Ether for all pages or relevant Search landing pages.

This can be accomplished by removing them via hooks, which will also allow you do to some things with the information.

Or disabled completely with inject scripts option.

Conclusion

The web vitals discourage the usage of systems that perform client side hydration and use large amounts of JavaScript on the client side.

If this is balanced by the UX or the enhanced development velocity depends on the specific business, its reliance on Google Search traffic and its competition.

As a positive, i do hope that this is a sign for framework & library developers to have a stronger focus on these factors as well.

I will try to add some of these best practices to a simple nuxt application on github, feel free to add ideas of your own!