react-lazy-hydration-hook
v1.1.0
Published
A React hook/HOC to lazy hydrate components on user interaction (hover, focus, keyboard)
Maintainers
Readme
React Lazy Hydration Hook (react-lazy-hydration-hook)
A lightweight, high-performance React hook and Higher-Order Component (HOC) designed for lazy hydration of server-rendered (SSR) components.
🚀 Installation
Via npm:
npm install react-lazy-hydration-hookVia yarn:
yarn add react-lazy-hydration-hookVia pnpm:
pnpm add react-lazy-hydration-hook⚡ What is it & Why use it?
Standard Server-Side Rendering (SSR) generates 100% of your HTML on the server, which is great for SEO and initial visual paint.
However, once the page arrives in the browser, React executes Full Hydration:
- React traverses 100% of the DOM tree generated by the server.
- It instantiates internal component states and attaches event listeners to every single node.
- The Problem: On complex pages (e.g. e-commerce grids, product feeds, footers, customer reviews), this initial JavaScript execution freezes the Main Thread and degrades key Core Web Vitals metrics (INP - Interaction to Next Paint, TBT - Total Blocking Time).
The Solution: useStatic (Islands Architecture)
useStatic defers client-side hydration until the user actually interacts with a specific component (hover, click, focus, or keyboard event).
- 100% SEO & SSR Preserved: The HTML generated on the server remains intact and fully visible to search engines.
- Instant Interactivity: The browser doesn't execute unnecessary JavaScript for off-screen or unvisited components on page load.
📈 Real-World Performance Benchmarks
Tested using hydrateRoot (React 18/19) in a real client DOM environment with 500 complex components:
| Metric / Benchmark | Standard React Hydration | Deferred Hydration (useStatic) | Performance Impact |
| :--- | :--- | :--- | :--- |
| Initial Hydration Time (Page Load) | 7.53 ms | 1.35 ms | 🚀 +82.1% Faster Initial Interactivity |
| Active React Memory Nodes at Boot | 500 / 500 nodes | 0 / 500 nodes | 📉 -100% initial JavaScript execution overhead |
| Server HTML Output Size | 100% complete | 100% complete | ✅ 0% Loss in SEO or DOM Structure |
| Main Thread Blocking (TBT) | Heavy load spikes | Free for smooth scrolling | 🟢 100/100 Core Web Vitals Potential |
Benchmark conducted using JSDOM client emulation on 500 nested interactive components.
⚙️ How It Works Under the Hood
- Server-Side Rendering (SSR): Your component renders normally on the server, producing standard HTML (e.g.
<button>Click</button>). - Client-Side Bypass: On initial page load,
useStaticrenders a memoized wrapper (SafeStaticHTML) withsuppressHydrationWarningand empty inner HTML. React skips DOM reconciliation for this tree, keeping the server-rendered HTML intact. - Event Interception: A lightweight native event listener (
pointerover,focusin,keydown) is attached to the wrapper element. - On-Demand Hydration: As soon as the user hovers or interacts with the element, React hydrates the full component tree and attaches interactive handlers seamlessly.
🛠️ Usage & Examples
1. Basic Usage (Lazy Hydration on Hover / Focus)
import { useStatic } from 'react-lazy-hydration-hook';
// A heavy component (e.g., product card, reviews list, comments section)
function CustomerReviews() {
return (
<div className="reviews-container">
<h2>Customer Reviews (50+)</h2>
{/* Heavy rendering logic */}
</div>
);
}
// Wrap it with useStatic
export const LazyCustomerReviews = useStatic(CustomerReviews, {
on: ['pointerover', 'focusin', 'click'],
});2. Static Only Components (ssrOnly)
For components that render pure HTML and never need client-side React state or click handlers (e.g. Technical Specs, Legal Footers, Static Descriptions):
import { useStatic } from 'react-lazy-hydration-hook';
import TechnicalSpecifications from './TechnicalSpecifications';
// Never hydrates on the client, saving 100% of JS execution cost forever
export const StaticSpecs = useStatic(TechnicalSpecifications, {
ssrOnly: true,
});3. Custom Wrapper Element & Hydration Callbacks
import { useStatic } from 'react-lazy-hydration-hook';
import HeavyWidget from './HeavyWidget';
export const CustomWidget = useStatic(HeavyWidget, {
on: 'click',
noWrapper: 'article', // Use <article> wrapper tag instead of <div>
wrapperProps: { className: 'widget-container', id: 'my-widget' },
didHydrate: () => {
console.log('HeavyWidget has been successfully hydrated on demand!');
},
});📖 API Reference
useStatic(Component, options?)
Options (LazyHydrationOptions)
| Option | Type | Default | Description |
| :--- | :--- | :--- | :--- |
| on | string \| string[] | ['pointerover', 'pointerdown', 'focusin', 'keydown', 'click'] | DOM event(s) that trigger client-side hydration. |
| ssrOnly | boolean | false | When true, the component renders on the server but never hydrates on the client. |
| noWrapper | boolean \| string | false | Customizes the HTML tag wrapper (e.g. 'span', 'article'). |
| wrapperProps | React.HTMLAttributes | {} | Props passed to the wrapper HTML element (className, id, etc.). |
| didHydrate | () => void | undefined | Callback fired immediately after hydration completes. |
🎯 Ideal Use Cases
- E-Commerce Product Pages: Keep reviews, recommendations, and specs static until hover.
- Infinite Scroll & Long Feeds: Defer hydration of off-screen feed posts.
- Footers & Sidebars: Prevent heavy secondary navigation from blocking initial load.
- Modals & Tooltips: Hydrate interactive overlays only when triggered.
📄 License
MIT © morganluc-ops
