Would you like to buy me a ☕️ instead?
Performance is a huge topic in the web dev world. Furthermore, performance is especially a huge topic in the context of SPAs. Ironically, performance is frequently stated as one of the biggest benefits and also as one of the most pressing concerns when it comes to this architectural pattern. While subsequent page views are typically very fast with client-side rendered applications, the initial page load can require to load (and even more importantly: to parse) a few megabytes (!) of JavaScript.
Nuxt.js and other frameworks promise to help with the initial page load dilemma which developers of large scale Vue.js applications often have to deal with. But there comes the next problem: rehydrating server-side rendered applications is also a massive burden on the CPU, and it shows in benchmarks like Lighthouse.
If you want to dive directly into the code, you can take a look at the GitHub repository for the demo application and you can also check out the vue-lazy-hydration plugin GitHub project.
Solutions to high Estimated Input Latency and Time to Interactive
One of the most important techniques to deal with those kind of problems is code splitting on a per route level. Nuxt.js and most other tools with SSR baked in (like VuePress or Gridsome) already handle this automatically for you, so in my opinion, this problem is solved.
But we can also use code splitting on a per component level as you can see in the following example.
<template>
<div class="MyComponent">
<ImageSlider v-if="showImageSlider"/>
</div>
</template>
<script>
export default {
components: {
ImageSlider: () => import('./ImageSlider.vue'),
},
data() {
return {
showImageSlider: false,
};
},
// ...
};
</script>
So far so good, but although this is another step in the right direction there is still a lot of room for improvement.
Consider the following example: a very long page is viewed on a small screen, there are a lot of components rendered which are not even visible to the user and they might very well never be if the user decides to navigate to the next page without scrolling. What a waste of resources.
The vue-lazy-hydration plugin
Over the last couple of days I was working on the vue-lazy-hydration Vue.js plugin. This plugin makes it pretty easy to utilize certain techniques for lazy loading Vue.js components on-demand. Furthermore it makes it possible to delay the hydration of server-side rendered HTML until it’s really needed. In the following examples we‘ll use vue-lazy-hydration
to improve the Estimated Input Latency of our demo application.
Conditionally loading components
I’ve already written an article about conditionally loading components as soon as they become visible. But as of writing the previous article I did not realize the further implications of this technique when combined with SSR. Instead of rendering a placeholder box, like in the example of the article, we can do without such tricks because the user already sees the pre-rendered HTML which is generated on the server. So we can conditionally load components as soon as they become visible without our users noticing any of it.
Load and hydrate components based on visibility
First we have to install the vue-lazy-hydration
package via npm so we can use it in our application.
npm install vue-lazy-hydration
Now we can import the <LazyHydrate>
wrapper component and use it to only hydrate components which are actually visible to the user.
<template>
<div class="IndexPage">
<!-- ... -->
<LazyHydrate when-visible>
<ImageSlider/>
</LazyHydrate>
<!-- ... -->
</div>
</template>
<script>
import LazyHydrate from 'vue-lazy-hydration';
export default {
name: 'IndexPage',
components: {
// ...
LazyHydrate,
ImageSlider: () => import('../components/ImageSlider.vue'),
// ...
},
// ...
};
</script>
In the example above you can see how to use the <LazyHydrate>
component to dynamically hydrate components only when they become visible.
One thing to keep in mind: if we don’t add a condition directive onto the wrapped component, the component bundle is still loaded immediately. But we can change that by adding a condition.
<template>
<div class="IndexPage">
<!-- ... -->
<LazyHydrate when-visible>
<ImageSlider
slot-scope="{ hydrated }"
v-if="hydrated"
/>
</LazyHydrate>
<!-- ... -->
</div>
</template>
The <LazyHydrate>
wrapper component passes a hydrated
property to its child component. We can use this property to only load the <ImageSlider>
component bundle if it is actually hydrated.
Hydrate components as soon as users start interacting
Next we take a look at the on-interaction
loader mode provided by vue-lazy-hydration
.
<template>
<div class="IndexPage">
<!-- ... -->
<LazyHydrate when-visible>
<ImageSlider/>
</LazyHydrate>
<LazyHydrate :on-interaction="['click', 'focus']">
<AppCounter/>
</LazyHydrate>
<!-- ... -->
</div>
</template>
<script>
import LazyHydrate from 'vue-lazy-hydration';
export default {
name: 'IndexPage',
components: {
// ...
LazyHydrate,
ImageSlider: () => import('../components/ImageSlider.vue'),
AppCounter: () => import('../components/AppCounter.vue'),
// ...
},
// ...
};
</script>
Above you can see how we can use the on-interaction
property to initialize the AppCounter
component in a way that it is only really hydrated as soon as either a click
event or a focus
event is fired.
Not loading and hydrating components at all
So much about conditionally hydrating components. But what about all those components which are only representational? They might only show some text and an image but they don’t have any functionality other than just rendering content. Why even bother with loading the JavaScript code and doing expensive rehydrating of server-side rendered code if the rendered HTML output of those components doesn’t change no matter what?
<template>
<div class="IndexPage">
<!-- ... -->
<LazyHydrate when-visible>
<ImageSlider/>
</LazyHydrate>
<LazyHydrate :on-interaction="['click', 'focus']">
<AppCounter/>
</LazyHydrate>
<LazyHydrate ssr-only>
<AppMediaObject
slot-scope="{ hydrated }"
v-if="hydrated"
/>
</LazyHydrate>
<!-- ... -->
</div>
</template>
<script>
import LazyHydrate from 'vue-lazy-hydration';
export default {
name: 'IndexPage',
components: {
// ...
LazyHydrate,
ImageSlider: () => import('../components/ImageSlider.vue'),
AppCounter: () => import('../components/AppCounter.vue'),
AppMediaObject: () => import('../components/AppMediaObject.vue'),
// ...
},
// ...
};
</script>
In the example code snippet above, the AppMediaObject
component is only ever loaded when the page is rendered on the server. No expensive parsing of JavaScript code or rehydrating of pre-rendered HTML necessary.
Do you want to learn more about advanced Vue.js techniques?
Register for the Newsletter of my upcoming book: Advanced Vue.js Application Architecture.
Benchmarks
I’ve built a little demo application to do some benchmarks. There are two versions of the application. One which does not utilize the techniques described above and another one which does. Let’s see the results.
As you can see in the screenshots above, both Estimated Input Latency and Time to interactive, are much lower in the second screenshot which shows the Lighthouse results of the optimized version of the demo application.
Wrapping it up
Lazy loading and hydration of Vue.js components for SSR powered applications can bring huge performance benefits. Keep in mind though, that this technique is highly experimental and as of writing this the vue-lazy-hydration
plugin is in a very early alpha stage.
If you’re trying out vue-lazy-hydration
yourself, please let me know how it’s going.
Special thanks to Rahul Kadyan who took up my initial idea and improved it greatly. The latest version of vue-lazy-hydration
is basically an opinionated fork of his lazy hydration package: lazy-hydration.