Home / Blog / Lazy Hydration and Server Components in Nuxt – Vue.js 3 Performance
Lazy Hydration and Server Components in Nuxt – Vue.js 3 Performance

Lazy Hydration and Server Components in Nuxt – Vue.js 3 Performance

Filip Rakowski
Filip Rakowski
Updated: May 16th 2024
This entry is part 2 of 2 in the series Vue.js 3 Performance

How does Hydration work in Nuxt.js?

Before we get into the details of optimizing components, we need to understand how exactly they can decrease the performance of our applications. To get there, we need to understand the process of hydration.

Hydration, in short, is a process of making static HTML of Server-Side Rendered (or Statically Rendered) applications interactive.

You probably noticed that sometimes, when you click on the interactive element of a Nuxt.js application right after it appears on the screen, nothing happens. Nothing happens because when it arrives in the browser, it’s not yet an interactive Single-Page application, it’s just a static HTML file. Right after the file is downloaded, the browser detects the <script> tags and downloads the code that turns static HTML elements into interactive Vue.js components. Basically, the same code that has run on the server to generate the HTML file now runs again in the browser to make it interactive. This process is called hydration.

To hydrate a component, two things need to happen:

  1. The code of a component we want to hydrate must be downloaded. In most cases, it’s already a part of the main bundle until it’s explicitly code-split into a separate file (we will learn later why code-splitting components should almost always be a default).
  2. This code needs to be executed to create a component instance and mount it on the already existing DOM node.

Most of the components don’t need to be eagerly hydrated

Hydration is an eager process, which means that it has to run on all of our components. Unfortunately, it’s the least optimal approach from the performance point of view. Each component contributes more JavaScript code to your bundle. The more JavaScript code we need to run, the longer the hydration process takes, and the more likely it is that the users will click on an element that simply doesn't work or that their first input (like click) will be delayed. While this is usually not a big issue on laptops, this “gap” could take even more than 10 seconds on mobile devices. Many users will assume that the application is broken and leave at this point.

Now that we have established that eager hydration is the root of all evil let’s consider whether this is even a necessary one. Component interactivity is very rarely needed upfront in general. Almost always, there is a trigger like click or hover - for example, the image gallery needs to be interactive only when you want to change an image, tabs need to be interactive only when you need to change the tab, chat widget, or modal window needs to be interactive only when you want to open it, etc. Based on that, we can conclude that most components do not have to be hydrated eagerly or, sometimes, to be hydrated at all!

Let’s prove it on the example - take a look at this example Product Page from a Vue Storefront demo.

The part with a green overlay is the one that we see on the screen when page loads, rest is below the fold. Let’s take a look at the code of the above page:

<template>
  <NuxtLayout name="default" :breadcrumbs="breadcrumbs">
    <NarrowContainer>
      <div class="md:grid gap-x-6 grid-areas-product-page grid-cols-product-page">
        <section class="grid-in-left-top md:h-full xl:max-h-[700px]">
           <Gallery :images="product?.gallery ?? []" />
        </section>
        <section class="mb-10 grid-in-right md:mb-0">
           <UiPurchaseCard v-if="product" :product="product" />
        </section>
        <section class="grid-in-left-bottom md:mt-8">
          <UiDivider class="mb-6" />
          <ProductProperties v-if="product" :product="product" />
          <UiDivider class="mt-4 mb-2 md:mt-8" />
          <ProductAccordion v-if="product" :product="product" />
            <UiDivider class="mt-4 mb-2" />
        </section>
      </div>
      <section class="mx-4 mt-28 mb-20">
                <!-- This is below the fold -->
        <RecommendedProducts v-if="recommendedProducts" :products="recommendedProducts" />
      </section>
    </NarrowContainer>
  </NuxtLayout>
</template>

We have five interactive components here:

  • Gallery
  • UiPurchaseCard that is responsible for “add to cart” functionality
  • ProductProperties that allows to select a different product configuration
  • ProductAccordion with full product description and customer reviews
  • RecommendedProducts

They all have one thing in common - they can remain completely static until the user interacts with them (eg. by clicking ). In some (many) cases, this could never happen - for example, a user could never click on the slider below, yet we download and execute it’s code.

If our components do not need to be hydrated eagerly, wouldn’t it be better to perform this process lazily? This way, we will make the hydration almost instantaneous and execute only the needed code.

While Nuxt doesn’t work this way out of the box, lazy hydration can be achieved very easily with a third-party library called nuxt-lazy-hydrate (which is a Nuxt wrapper for a Vue 3 Lazy Hydration Plugin that can be used for any server-side-rendered Vue.js application). It’s dead simple to use.

First, install the module in your Nuxt application:

npm install nuxt-lazy-hydrate
// nuxt.config.js
{
  modules: [
        "nuxt-lazy-hydrate"
    ],
}

Once it’s installed, wrap the desired piece of the UI and specify when it should be hydrated using one of the available directives:

  • when-idle runs when the main thread work is done and the browser goes idle. It’s important to be aware that it is using window.requestIdleCallback under the hood which is not supported in Safari.
<NuxtLazyHydrate when-idle>
    <ProductSlider>
</NuxtLazyHydrate>
  • on-interaction as the name suggests runs only after interacts with a component in a certain way, eg. via click or focus.
<NuxtLazyHydrate :on-interaction="['click', 'touchstart']">
  <ProductSlider>
</NuxtLazyHydrate>
  • when-triggered allows you to bind a custom function that returns a boolean reactive value
<template>
    <NuxtLazyHydrate when-triggered="triggerHydration">
      <ProductSlider>
      </NuxtLazyHydrate>
</template>

<script setup>
    triggerHydration () {
      // custom logic for hydration trigger
    }
</script>
  • when-visible hydrates the component when it appears in the viewport
<NuxtLazyHydrate when-visible>
  <ProductSlider>
</NuxtLazyHydrate>
  • If you don’t use any properties on the hydration wrapper, the child component will never get hydrated
<NuxtLazyHydrate>
  <ProductSlider>
</NuxtLazyHydrate>

Now that you know what options are available, get back to the Product Page code snippet above and think about which components you would wrap with NuxtLazyHydrate component and what hydration conditions you would use. Once you do it, you can check how we did it but don’t get discouraged if it does not match your ideas. There is usually more than one good approach and as long as the component hydration is not blocking the main thread it’s a correct one.

How does lazy hydration work?

So, how does this plugin work, and what savings does it bring? As we learned a few paragraphs earlier, JavaScript code must be downloaded and executed to make a component dynamic. This is precisely what we are saving by leveraging the lazy hydration technique. At its core, it’s nothing else than the lazy loading of a component. The only difference between regular lazy loading and lazy hydration is that we also use it to hydrate a piece of static HTML in the latter.

Both internet bandwidth and CPU power can be spent on executing code that is needed immediately, and when that need is already fulfilled, we can download and execute the additional chunks of code at the most convenient moment without negatively impacting the performance of our website.

If you are curious about the exact mechanism of lazy hydration, you can check the source code of this composable from vue3-lazy-hydration library. It illustrates the concept quite well!

Nuxt Server Components and Nuxt Islands

I mentioned earlier that Nuxt does not provide any core utilities to delay the hydration of your components (except a very basic one that I'll mention about in a moment), but it comes with a handy solution to prevent it! Both will work exactly like a <NuxtLazyHydrate> component without any properties and do not require installing third-party libraries.

The first option is a <NuxtIsland> component. If you know the Astro Islands, you can think about the Nuxt ones as they’re opposite. While Astro has Islands of interactivity, Nuxt has islands of… non-interactivity. Wrapping anything with NuxtIsland just completely prevents it from being hydrated. Personally, I don’t think there is any notable difference between using <NuxtLazyHydrate> and <NuxtIsland>, but it’s always better to stick with official API’s than third-party ones. Some tiny internal optimizations could take this component into account, or they can be introduced in the future - you never know!

<NuxtIsland>
    <ProductCard />
</NuxtIsland>

Sometimes, we create components that are not supposed to be dynamic in any context of their usage, like text nodes or images. Wrapping such components With a Lazy Hydration Wrapper or Nuxt Island every time would produce a lot of additional boilerplate code. In such cases, Server Components can become handy. Server Component is a special type of component that never hydrates, but you decide about this on the file tree level instead of the code level, so all of its occurrences behave the same. To turn a regular component into a server one, you simply need to add server suffix to its name. For example, ProductCard.vue would be renamed to ProductCard.server.vue. You don’t need to add any suffix when you use it in your code.

// This will use ProductCard.server.vue
<ProductCard />

At the moment of writing this article, both features are still experimental, so you need to enable them in your nuxt.config.js

export default defineNuxtConfig({
  experimental: {
    componentIslands: true,
  },
})

Lazy Hydration in Nuxt core

At the time of writing this article, there is a proposal to add lazy hydration to Nuxt Core where Daniel Roe (Head of Nuxt Framework) confirmed that it will be added as a feature of the already existing <Lazy* component prefix.

daniel roe's comment on github

As a reminder adding a <Lazy* prefix to the component is what makes it lazy-loaded. For example, the below component will be loaded only when show is true

<LazyMountainsList v-if="show" />

We don’t know what is in Daniel’s head but if he decides to follow the proposal we can probably see something similar to this:

<LazyMountainsList hydrate:when-visible />

It's also worth mentioning that NuxtIsland has some basic capabilities of delaying hydration already via lazy="true" argument (it works similarly to when-visible directive from NuxtLazyHydrate) but considering the above statement of Daniel and that this feature is still experimental we can likely expect this syntax to look differently. How differently? It's hard to tell.

What metrics will benefit from lazy hydration?

Before we end, let’s talk a little bit about the impact of lazy hydration on performance metrics. Even if we intuitively know that something improves performance, we won’t be able to prove it without measuring the results!

Because eager hydration blocks the main thread, it can increase the delay between user action and application response (nothing will happen before hydration finishes). This impacts the First Input Delay core web vital (which will soon be replaced with Interaction to Next Paint).

In addition, the process of hydration itself will prolong the time needed for your app to become interactive, which is described by the Time to Interactive metric. Both metrics should visibly improve if you properly leverage lazy hydration techniques in your application.

Summary

Lazy Hydration is a powerful technique that could significantly reduce your Time to Interactive and First Input delay metric. While Nuxt doesn't offer any out-of-the-box features to allow i, the feature can be easily leveraged through third-party libraries. In addition, built-in features like Nuxt islands and Nust Server Components can already let you skip the hydration of certain components completely without installing any third-party libraries. In the next part of the series, we will look into practical examples of applying Lazy Hydration in your projects!

Related Courses

Start learning Vue.js for free

Filip Rakowski
Filip Rakowski
Co-founder & CTO of Vue Storefront - Open Source eCommerce Platform for developers. I'm passionate about sharing my knowledge and thoughts on software architecture, web performance, Vue and raising awaraness about business impact of the technology and code we write. When I'm not working I'm probably playing with one of my cats.

Comments

Latest Vue School Articles

From Vue.js Options API to Composition API: Is it Worth it?

From Vue.js Options API to Composition API: Is it Worth it?

Explore the technicalities of transitioning from Options API to Composition API in Vue.js. Discover if migrating your app is worth the effort in our detailed guide
Mostafa Said
Mostafa Said
What’s New in Nuxt 4

What’s New in Nuxt 4

Have anxiety about a new major version of Nuxt coming out? Worried about a big migration project? Don’t worry about it, a peaceful and easy upgrade is literally one of the features of Nuxt version 4.
Daniel Kelly
Daniel Kelly

Our goal is to be the number one source of Vue.js knowledge for all skill levels. We offer the knowledge of our industry leaders through awesome video courses for a ridiculously low price.

More than 200.000 users have already joined us. You are welcome too!

Follow us on Social

© All rights reserved. Made with ❤️ by BitterBrains, Inc.