Lazy hydration of server-side rendered Vue.js components.
vue-lazy-hydration
is a renderless Vue.js component to improve Estimated Input Latency and Time to Interactive of server-side rendered Vue.js applications. This can be achieved by using lazy hydration to delay the hydration of pre-rendered HTML.
npm install vue-lazy-hydration
import LazyHydrate from 'vue-lazy-hydration';
// ...
export default {
// ...
components: {
LazyHydrate,
// ...
},
// ...
};
In the example below you can see the four hydration modes in action.
<template>
<div class="ArticlePage">
<LazyHydrate when-idle>
<ImageSlider/>
</LazyHydrate>
<LazyHydrate ssr-only>
<ArticleContent :content="article.content"/>
</LazyHydrate>
<LazyHydrate when-visible>
<AdSlider/>
</LazyHydrate>
<!-- `on-interaction` listens for a `focus` event by default ... -->
<LazyHydrate on-interaction>
<CommentForm :article-id="article.id"/>
</LazyHydrate>
<!-- ... but you can listen for any event you want ... -->
<LazyHydrate on-interaction="click">
<CommentForm :article-id="article.id"/>
</LazyHydrate>
<!-- ... or even multiple events. -->
<LazyHydrate :on-interaction="['click', 'touchstart']">
<CommentForm :article-id="article.id"/>
</LazyHydrate>
</div>
</template>
<script>
import LazyHydrate from 'vue-lazy-hydration';
export default {
components: {
LazyHydrate,
AdSlider: () => import('./AdSlider.vue'),
ArticleContent: () => import('./ArticleContent.vue'),
CommentForm: () => import('./CommentForm.vue'),
ImageSlider: () => import('./ImageSlider.vue'),
},
// ...
};
</script>
- Because it is at the very top of the page, the
ImageSlider
should be hydrated eventually, but we can wait until the browser is idle. - The
ArticleContent
component is only loaded in SSR mode, which means it never gets hydrated in the browser, which also means it will never be interactive (static content only). - Next we can see the
AdSlider
beneath the article content, this component will most likely not be visible initially so we can delay hydration until the point it becomes visible. - At the very bottom of the page we want to render a
CommentForm
but because most people only read the article and don't leave a comment, we can save resources by only hydrating the component whenever it actually receives focus.
Attention: If your setup depends on the Vue.js template-renderer for server side rendering (which is the case for Nuxt.js and Gridsome), this technique currently doesn't work and JavaScript bundles are immediately loaded. See vuejs/vue#9847 for the progress on this.
Attention: There currently seems to be a problem with this approach in certain circumstances. See #19 for more information.
<template>
<div class="ArticlePage">
<LazyHydrate on-interaction>
<CommentForm
slot-scope="{ hydrated }"
v-if="hydrated"
:article-id="article.id"
/>
</LazyHydrate>
<!-- Or using new Vue.js 2.6.x v-slot syntax -->
<LazyHydrate
v-slot="{ hydrated }"
on-interaction
>
<CommentForm
v-if="hydrated"
:article-id="article.id"
/>
</LazyHydrate>
</div>
</template>
<script>
import LazyHydrate from 'vue-lazy-hydration';
export default {
components: {
LazyHydrate,
// The `CommentForm` is only imported if `hydrated` is true.
CommentForm: () => import('./CommentForm.vue'),
},
// ...
};
</script>
Sometimes you might want to prevent a component from loading initially but you want to activate it on demand if a certain action is triggered. You can do this by manually triggering the component to hydrate like you can see in the following example.
<template>
<div class="MyComponent">
<button @click="editModeActive = true">
Activate edit mode
</button>
<LazyHydrate ssr-only :trigger-hydration="editModeActive">
<UserSettingsForm/>
</LazyHydrate>
</div>
</template>
<script>
import LazyHydrate from 'vue-lazy-hydration';
export default {
components: {
LazyHydrate,
UserSettingsForm: () => import('./UserSettingsForm.vue'),
},
data() {
return {
editModeActive: false,
};
},
// ...
};
</script>
Because of how this package works, it is not possible to nest multiple root nodes inside of a single <LazyHydrate>
. But you can wrap multiple components with a <div>
.
<template>
<div class="MyComponent">
<LazyHydrate ssr-only>
<div>
<ArticleHeader/>
<ArticleContent/>
<ArticleMetaInfo/>
<ArticleFooter/>
</div>
</LazyHydrate>
</div>
</template>
The (one and only) immediate child of <LazyHydrate>
must always exist, i.e. there can be no v-if
on the <div>
(if the v-if
is needed, it needs to be on the <LazyHydrate>
)
Internally the Intersection Observer API is used to determine if a component is visible or not. You can provide Intersection Observer options to the when-visible
property to configure the Intersection Observer.
<template>
<div class="MyComponent">
<LazyHydrate :when-visible="{ rootMargin: '100px' }">
<ArticleFooter/>
</LazyHydrate>
</div>
</template>
For a list of possible options please take a look at the Intersection Observer API documentation on MDN.
Attention: because of a bug in Vue.js <= v2.6.7 Import Wrappers require that you have at least version v2.6.8 of Vue.js installed otherwise they will not work correctly in certain situations (especially in combination with Vue Router).
Additionally to the <LazyHydrate>
wrapper component you can also use Import Wrappers to lazy load and hydrate certain components.
<template>
<div class="ArticlePage">
<ImageSlider/>
<ArticleContent :content="article.content"/>
<AdSlider/>
<CommentForm :article-id="article.id"/>
</div>
</template>
<script>
import {
hydrateOnInteraction,
hydrateSsrOnly,
hydrateWhenIdle,
hydrateWhenVisible,
} from 'vue-lazy-hydration';
export default {
components: {
AdSlider: hydrateWhenVisible(
() => import('./AdSlider.vue'),
// Optional.
{ observerOptions: { rootMargin: '100px' } },
),
ArticleContent: hydrateSsrOnly(
() => import('./ArticleContent.vue'),
{ ignoredProps: ['content'] },
),
CommentForm: hydrateOnInteraction(
() => import('./CommentForm.vue'),
// `focus` is the default event.
{ event: 'focus', ignoredProps: ['articleId'] },
),
ImageSlider: hydrateWhenIdle(() => import('./ImageSlider.vue')),
},
// ...
};
</script>
- Properties passed to a wrapped component are rendered as an HTML attribute on the root element.
E.g.<ArticleContent :content="article.content"/>
would render to<div class="ArticleContent" content="Lorem ipsum dolor ...">Lorem ipsum dolor ...</div>
as long as you don't providecontent
as an ignored property the way you can see in the example above. - When using
hydrateWhenVisible
andhydrateOnInteraction
all instances of a certain component are immediately hydrated as soon as one of the instances becomes visible or is interacted with.
This plugin will not work as advertised if you're not using it in combination with SSR. Although it should work with every pre-rendering approach (like Prerender SPA Plugin, Gridsome, ...) I've only tested it with Nuxt.js so far.
- abomination: a Concept for a Static HTML / Dynamic JavaScript Hybrid Application
- How to Drastically Reduce Estimated Input Latency and Time to Interactive of SSR Vue.js Applications
The code of the current implementation of this package is based on a similar package created by Rahul Kadyan. Thanks to his code I'm finally able to build a clean solution for what I dreamed of when I created the abomination.
Markus Oberlehner
Website: https://markus.oberlehner.net
Twitter: https://twitter.com/MaOberlehner
PayPal.me: https://paypal.me/maoberlehner
Patreon: https://www.patreon.com/maoberlehner
MIT