Keep below-the-fold components server-rendered but defer their JavaScript and hydration until they approach the viewport, using Vue 3.5's defineAsyncComponent({ hydrate: hydrateOnVisible() }) — or Nuxt's <LazyX hydrate-on-visible /> — so the HTML is visible immediately and only the interactivity waits for an IntersectionObserver.

Problem / Scenario Context

A Nuxt product page server-renders a review section with a rich filtering widget, a Q&A accordion and a recommendations carousel, all near the bottom. Hydration of these components on page load takes 250 ms on a mid-range phone, most of it for code the user might never interact with, and it delays the "Add to cart" button's first interaction. Replacing them with client-only lazy components removes the cost but also removes the server-rendered content: reviews disappear from the initial HTML, search engines lose them, and the page jumps when they load.

What the page needs is lazy hydration: server-render the HTML as usual, but defer hydrating it. The Vue Observer Composables topic covers composables; this page applies observers to hydration.

Mechanics Explanation

Hydration attaches Vue's component instances, reactivity and event listeners to server-rendered DOM. Until a component is hydrated, its HTML is visible but inert — clicks on its buttons do nothing.

Vue 3.5 lazy hydration. defineAsyncComponent accepts a hydrate strategy for server-rendered async components. hydrateOnVisible(options) uses an IntersectionObserver (accepting rootMargin/threshold) on the component's root elements: until they intersect, the component's code is not loaded and its DOM is not hydrated; on intersection, Vue loads the chunk and hydrates in place. Other strategies include hydrateOnIdle, hydrateOnInteraction and hydrateOnMediaQuery.

Nuxt (3.16+ / 4) exposes the same strategies declaratively on auto-imported Lazy components: <LazyReviewFilters hydrate-on-visible />, with variants such as hydrate-on-idle, hydrate-on-interaction and hydrate-never.

Because the HTML is server-rendered, nothing shifts when hydration happens. The risk is the opposite: a user who scrolls to the section and clicks before hydration completes gets no response. hydrateOnInteraction replays the triggering event after hydration; hydrateOnVisible with a generous rootMargin usually hydrates before the user arrives.

Eager Versus Visibility-Triggered HydrationTwo lanes over five seconds on a mid-range phone. With eager hydration, the review, Q&A and carousel components hydrate right after page load, delaying when the add-to-cart button becomes responsive. With visibility-triggered hydration, only the above-the-fold components hydrate at load, and the review components load and hydrate when the user scrolls near them later.Product page, mid-range phoneeagerabove fold3 widgets hydratecart readyon visibleabove foldcart readyreviews hydrate0s1s2s3s4s5s

Comparison Table: Loading Strategies for Below-the-Fold Components

Strategy In initial HTML JS on load Interactive when reached CLS risk
Eager SSR + hydration yes all yes none
Client-only lazy (<ClientOnly> / ssr: false) no none after load high
Lazy hydration on visible yes none until near yes, if margin suffices none
Lazy hydration on interaction yes none until touched replays the first event none
Never hydrate (static) yes none never none

Minimal Reproducible Example

VUE
<!-- pages/product/[id].vue -->
<template>
  <ProductHero :product="product" />
  <AddToCart :product="product" />
  <ReviewFilters :reviews="reviews" />      <!-- hydrates on load: 120 ms -->
  <QAAccordion :items="qa" />               <!-- hydrates on load: 60 ms -->
  <Recommendations :ids="recs" />           <!-- hydrates on load: 70 ms -->
</template>

The Performance panel shows a long hydration task right after load, and interaction with Add to cart is delayed while it runs.

Production-Safe Solution

VUE
<!-- Nuxt: declarative lazy hydration on auto-imported Lazy components -->
<template>
  <ProductHero :product="product" />
  <AddToCart :product="product" />
  <LazyReviewFilters hydrate-on-visible :reviews="reviews" />
  <LazyQAAccordion hydrate-on-interaction :items="qa" />
  <LazyRecommendations hydrate-on-visible :ids="recs" />
</template>
TypeScript
// Plain Vue 3.5 SSR: the same with defineAsyncComponent.
import { defineAsyncComponent, hydrateOnVisible } from 'vue';

export const ReviewFilters = defineAsyncComponent({
  loader: () => import('./ReviewFilters.vue'),
  hydrate: hydrateOnVisible({ rootMargin: '300px' }),     // start before it is on screen
});

The review filters hydrate when they come within 300 px of the viewport; the Q&A accordion waits for the first click or focus and replays it after hydrating, so the first toggle still works; recommendations hydrate on visibility. None of them ship JavaScript on initial load, all of them are in the server HTML.

For versions without built-in strategies, a manual fallback wraps server-rendered content and mounts the interactive component when visible — accepting that, without true lazy hydration, the wrapper's content is re-rendered by the client component rather than hydrated:

VUE
<!-- LazyOnVisible.vue: fallback for older setups -->
<script setup lang="ts">
import { ref, onMounted, onBeforeUnmount } from 'vue';
const root = ref<HTMLElement>(); const show = ref(false); let io: IntersectionObserver | undefined;
onMounted(() => {
  io = new IntersectionObserver(([e]) => { if (e.isIntersecting) { show.value = true; io?.disconnect(); } },
    { rootMargin: '300px' });
  io.observe(root.value!);
});
onBeforeUnmount(() => io?.disconnect());
</script>
<template>
  <div ref="root"><slot v-if="show" /><slot v-else name="placeholder" /></div>
</template>

Which Hydration Strategy for This Component?A decision chain. If the component is above the fold or needed immediately, hydrate eagerly. Otherwise, if users interact with it only occasionally and the first interaction can be replayed, hydrate on interaction. Otherwise, if it should be ready by the time the user scrolls to it, hydrate on visible with a margin. Otherwise, if it is purely presentational, never hydrate it.Above the fold or needed immediately?Eager hydrationyesnoOccasional use; first event can be replayed?hydrate-on-interactionyesnoShould be ready when scrolled to?hydrate-on-visible with a marginyesnoPurely presentational: hydrate-never.

Pitfalls

Hydration mismatches surface later. A component whose server and client renders differ used to warn on page load; with lazy hydration it warns — and may re-render — when it scrolls into view, where a visual correction is more noticeable. Keep observer-driven state out of server rendering; see hydration mismatch from observer-driven class toggles.

Props and data are still serialised. The component's props travel in the page payload even if it never hydrates. Lazy hydration saves JavaScript and hydration work, not payload size.

Anchors and deep links. A user arriving at #reviews lands on the component immediately; hydrateOnVisible fires right away, which is correct. With hydrate-on-interaction, the first click is replayed, so it also works.

Nested lazy components each wait for their own trigger. A lazily hydrated parent must hydrate before its children can, so keep lazy boundaries at one level where possible.

Hydration Work on Initial LoadA bar chart of main-thread time spent hydrating on initial load of a product page on a mid-range phone. Eager hydration of all components took about three hundred and ten milliseconds. Lazy hydration of the three below-the-fold components reduced it to about sixty milliseconds, the cost of the above-the-fold components only.Hydration on load, product page, mid-range phoneeager, all components~310 msbelow-the-fold lazy-hydrated~60 ms

Verification Steps

  • View source: lazily hydrated components' HTML must be present.
  • Network panel: their chunks load only when scrolled near (or interacted with).
  • Performance panel: the load-time hydration task shrinks; interaction with above-the-fold controls is faster.
  • Scroll quickly and click a lazily hydrated control; it must respond (with enough margin or event replay).
  • Check the console for hydration mismatch warnings when components hydrate.

Common Mistakes to Avoid

  • Client-only rendering for SEO-relevant content. It removes the HTML; use lazy hydration instead.
  • Lazy-hydrating above-the-fold controls. They become unresponsive at the worst moment.
  • A zero margin for visible hydration. Users reach the component before it is interactive.
  • Rendering observer state on the server. Mismatches appear when the component hydrates.

FAQ

What is the difference between lazy loading and lazy hydration?

Lazy loading defers rendering the component at all, so its HTML is missing until it loads. Lazy hydration renders the HTML on the server immediately and defers only the JavaScript that makes it interactive.

Which Vue version supports hydration strategies?

Vue 3.5 added the hydrate option to defineAsyncComponent with hydrateOnVisible, hydrateOnIdle, hydrateOnInteraction and hydrateOnMediaQuery. Nuxt exposes them as props on Lazy components in recent versions.

What happens if the user clicks before hydration?

With hydrate-on-visible, nothing, until hydration completes, which is why a margin matters. hydrate-on-interaction captures the triggering event and replays it after hydration, so the first click works.

Does lazy hydration reduce the HTML payload?

No. The server-rendered HTML and the component's serialised props are still sent. It reduces JavaScript loaded on startup and the main-thread time spent hydrating.

Can I use my own IntersectionObserver options?

Yes. hydrateOnVisible accepts IntersectionObserver options such as rootMargin and threshold, which is how you start hydration before the component is actually on screen.

How do I test that lazy hydration works?

In an end-to-end test, load the page, assert the component's text is present in the HTML, assert its chunk has not been requested, scroll it into view, wait for the chunk request, then interact with it and assert the response. That covers content, deferral and interactivity in one test.

Can analytics or impressions run before the component hydrates?

Not from the component's own code, which has not loaded. Track impressions with a page-level observer on the server-rendered element instead, so they do not depend on hydration timing.

Is lazy hydration useful for components with no interactivity?

Components with no interactivity do not need hydration at all. hydrate-never, or rendering them as plain server components, avoids shipping their JavaScript entirely.


↑ Back to Vue Observer Composables