Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Lazy Hydration in Nuxt Core #24242

Open
4 tasks done
filrak opened this issue Nov 10, 2023 · 12 comments
Open
4 tasks done

Lazy Hydration in Nuxt Core #24242

filrak opened this issue Nov 10, 2023 · 12 comments

Comments

@filrak
Copy link

filrak commented Nov 10, 2023

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

@yooneskh
Copy link

Hi All, Thank you very much for your great work!

When using the directive approach, will the logic itself be lazy loaded in our bundle? Or even if we do not use it, it will increase the size? I am talking about the size of nuxt itself.

Also will the component be available too? It seems kind of nice in some situations.

@filrak
Copy link
Author

filrak commented Nov 10, 2023

Hey @yooneskh . I think a fair assumption would be to always code-split the lazy-loaded chunks.

@yooneskh
Copy link

@filrak thanks for your response, i don't think i explained it correctly.

What i though was that when the lazy hydration was a component, if we didn't use it, the logic for lazy hydrating a piece of the app would not be loaded (seperate from how that piece of the app will be loaded).

But if it is a directive, we cannot treeshake the logic of lazy loading away. Kind of like how Vue 3 itself treeshakes parts of its functionalities away if they are not used.

@michalkuncio
Copy link

Great proposal! I also thought about this as a part of the core package. I would love it, and I think directives are the way to go in that subject.

@filrak
Copy link
Author

filrak commented Nov 10, 2023

@yooneskh ok, I get your point now. Great question!

I think the size of the directive itself will be insignificant, but I think the usage of the directive in the users' code can be easily detected during the build step. Therefore, it can be removed from the bundle if not used. There might be some obstacles I am unaware of, but for now, it seems entirely doable to me.

@robokozo
Copy link
Sponsor

I think it's a cool idea. Reminds me of astro style template directives

@yooneskh
Copy link

@filrak That makes sense, thanks!

@kissu
Copy link

kissu commented Nov 13, 2023

I think it's a cool idea. Reminds me of astro style template directives

Indeed, you posted it faster than me haha but here was the original discussion 👌🏻
withastro/astro#877

That lead towards some custom API to create your own directives: https://docs.astro.build/en/reference/directives-reference/#custom-client-directives

@louiss0
Copy link

louiss0 commented Nov 16, 2023

This API is awesome it's explicit and it's declarative. It even covers most of the use cases.

@fago
Copy link

fago commented Nov 19, 2023

This suggestion seems awesome!

One thought: It might be useful to define a default hydration behaviour for a component, like making it a server component. e.g. hydrate:when-visible would be a reasonable default for a lot of components, maybe even a reasonable global default! Given different defaults, we'd have to add a hydrate:immediate option as well.

@gouthamrangarajan
Copy link

Hi great suggestion,
Minor typos(not a big deal)
"The truth is - we rarely need interactivity upront." I think you mean Upfront
"when we want to hydrate a set of compoennts in particular order". I think you mean components

@danielroe
Copy link
Member

We'd like to explore expanding the ability of <Lazy- prefixed Nuxt components and think this would be nice approach with good DX. The options could be passed through a prop/directive, and we could keep this simpler to start with (a promise-based or boolean flag to control hydration).

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

10 participants