-
-
Notifications
You must be signed in to change notification settings - Fork 5.4k
Description
Describe the feature
What?
This proposal introduces lazy hydration feature as a first-class citizen in Nuxt Core.
This RFC is inspired by a Vue 3 Lazy Hydration Plugin from Freddy Escobar (that was inspired by another plugin from Markus Oberlehner). All the credit belongs to these amazing developers. I am literally suggesting implementing it in the core with some tiny DX improvements.
Why?
Large JavaScript bundles are one of the key reasons behind poor performance of Single Page Applications. Every interactive element you add to the project at the component level is by default loaded eagerly during the application start-up, often blocking the critical rendering path.
The truth is - we rarely need interactivity upront. It's usually triggered - sometimes by an event like click, sometimes element appears in the viewport, sometimes it can be loaded when the browser is done with more important tasks and goes idle. Despite interactivity not being eagerly needed in most cases, we are lacking first-class solution allowing to control hydration process in Nuxt. There is a <NuxtIsland>
component that moves the needle into the right direction but it's a 0/1 tool. If we're already going this direction, let's release a complete solution that tackles the problem as a whole.
We see lazy hydration being implemented as a first-class citizen in (meta)frameworks like Astro or Qwik with great results that prove that interactivity is rarely needed eagerly.
We need similar solution in Nuxt, coming from the core, integrated well with other Nuxt features and benefiting from framework-level optimizations.
How?
A natural temptation would be to introduce components similar to the one from Vue3 Lazy Hydrate library but this solution is not perfect DX-wise.
When you have multiple components requireing different treating you end up doubling the size of your files by creating a separate wrapper for each component.
Here is an example from Vue Storefront's boilerplate for Nuxt 3.
<div>
<section class="grid-in-left-top md:h-full xl:max-h-[700px]">
<NuxtLazyHydrate when-idle>
<Gallery :images="product?.gallery ?? []" />
</NuxtLazyHydrate>
</section>
<section class="mb-10 grid-in-right md:mb-0">
<NuxtLazyHydrate when-idle>
<UiPurchaseCard v-if="product" :product="product" />
</NuxtLazyHydrate>
</section>
<section class="grid-in-left-bottom md:mt-8">
<UiDivider class="mb-6" />
<NuxtLazyHydrate when-visible>
<ProductProperties v-if="product" :product="product" />
</NuxtLazyHydrate>
<UiDivider class="mt-4 mb-2 md:mt-8" />
<NuxtLazyHydrate when-visible>
<ProductAccordion v-if="product" :product="product" />
</NuxtLazyHydrate>
</section>
<UiDivider class="mt-4 mb-2" />
</div>
<section class="mx-4 mt-28 mb-20">
<NuxtLazyHydrate when-visible>
<RecommendedProducts v-if="recommendedProducts" :products="recommendedProducts" />
</NuxtLazyHydrate>
</section>
A directive would be a much more elegant approach that fixes this small but definitely annoying DX flaw.
hydrate
directive
The idea is simple - introduce global hydrate
directive that can control when components are hydrated.
<!-- Hydrate when certain event is triggered -->
<MyComponent hydrate:on="['click', 'touchstart']" />
<!-- Hydrate when certain condition is met -->
<MyComponent hydrate:when="shouldHydrate === true" />
<!-- Hydrate when element is in the viewport -->
<MyComponent hydrate:when-visible />
<!-- Hydrate when element is in the viewport (with an offset) -->
<MyComponent hydrate:when-visible="{ rootMargin: '100px' }" />
<!-- Hydrate when browser is idle (requestIdelCallback) -->
<MyComponent hydrate:when-idle />
<!-- Hydrate when browser is idle with maximum timeout -->
<MyComponent hydrate:when-idle="4000" />
<!-- Never Hydrate, works just like <NuxtIsland> -->
<MyComponent hydrate:never />
Hydration callback
Having a hydration callback could make it easier to react to components hydration from it's parent.
<MyComponent @hydrated="callback" />
It would be useful when we want to hydrate a set of compoennts in particular order.
<template>
<MyComponentA hydrate:when="hydrateA" @hydrated="hydrateB = true" />
<MyComponentB hydrate:when="hydrateB" />
<button @click="hydrateA = true">Start hydration</button>
</template>
<script setup>
const hydrateA = ref(false)
const hydrateB = ref(false)
</script>
Hydration Wrapper
We can use already existing NuxtIsland
component to wrap multiple components/nodes at once.
<NuxtIsland hydrate:never>
<div> {{ message }} </div>
<MyComponentA />
<MyComponentB />
<MyComponentC />
</NuxtIsland>
Framework-level optimizations
Thanks to component auto-imports we can statically analyze the code and specify which components chunks should be lazy-loaded as build-time optimization.
Additional information
- Would you be willing to help implement this feature?
- Could this feature be implemented as a module?
Final checks
- Read the contribution guide.
- Check existing discussions and issues.