A React component for lazy hydration of components, allowing you to defer the hydration of server-side rendered components until they're needed.
Lazy hydration is a powerful optimization — skip hydrating non-critical components until they're actually needed — but existing approaches in React 18+ have a catch: the un-hydrated component temporarily renders as an empty string, causing layout shift (CLS) and a flash of missing content.
LazyHydration avoids this by keeping the server-rendered HTML on screen, untouched, until a trigger fires. Your users see the real content from the first paint; React just defers the work of attaching interactivity.
- Maintaining SEO-friendly server-rendered content
- Lazy hydration of React components
- Providing flexible triggers for when hydration should occur
- Intersection Observer support for viewport-based hydration
- Idle callback support for background hydration
- Event-based hydration triggers
refforwarding support- Legacy mode for pure client-side rendering scenarios
- Zero dependencies
This package is published to GitHub Packages.
- Create or edit
.npmrcin your project root:
@jyunhanlin:registry=https://npm.pkg.github.com
- Install the package:
npm install @jyunhanlin/react-lazy-hydrationLazyHydration preserves your server-rendered HTML until a trigger fires — so you usually don't need a fallback.
Basic usage:
import { LazyHydration } from '@jyunhanlin/react-lazy-hydration';
function App() {
return (
<LazyHydration intersectionObserver={{ rootMargin: '200px' }}>
<HeavyComponent />
</LazyHydration>
);
}The server-rendered HTML of HeavyComponent stays on screen immediately. Hydration is deferred until the wrapper scrolls within 200px of the viewport.
Combining triggers:
<LazyHydration
intersectionObserver={{ rootMargin: '200px' }}
idleCallback={{ timeout: 3000 }}
events={['mousemove']}
>
<HeavyComponent />
</LazyHydration>Any trigger that fires first wins. Above: hydrate when the component nears the viewport, after the browser goes idle (up to 3 seconds), or on the first mousemove — whichever happens first.
Client-only rendering (legacy mode):
<LazyHydration
legacy
fallback={<Skeleton />}
idleCallback={{ timeout: 1000 }}
>
<ClientOnlyWidget />
</LazyHydration>In legacy mode the wrapper renders empty on the client instead of preserving SSR HTML — use this for pure client-side apps or components that don't render on the server. fallback is shown in the empty slot until a trigger fires.
| Prop | Type | Required | Default | Description |
|---|---|---|---|---|
children |
ReactNode |
Yes | - | The component to be lazily hydrated |
fallback |
ReactNode |
No | - | Fallback content shown before hydration — only rendered when there's no SSR HTML to preserve (i.e. in legacy mode or pure CSR) |
intersectionObserver |
IntersectionObserverInit |
No | - | Hydrate when the wrapper intersects the viewport (passed to IntersectionObserver) |
idleCallback |
{ timeout?: number } |
No | - | Configuration for requestIdleCallback trigger |
events |
string[] |
No | [] |
Array of events that trigger hydration |
legacy |
boolean |
No | false |
Skip SSR HTML preservation, use empty innerHTML |
ref |
Ref<HTMLDivElement> |
No | - | Forwarded ref to the wrapper div |
- During server-side rendering, the component renders normally
- On the client, the server-rendered HTML stays visible as-is until hydration triggers — no flash, no layout shift. Internally the captured HTML is re-injected into the wrapper div so React can skip re-rendering it on mount
- Hydration is triggered based on configured conditions:
- When the component enters the viewport (Intersection Observer)
- When the browser is idle (
requestIdleCallback, falls back tosetTimeout) - When specified DOM events occur
- Once triggered, the component fully hydrates and becomes interactive
- Once hydrated, the component stays hydrated across re-renders
When legacy is enabled, step 2 is skipped — the wrapper renders with empty innerHTML instead of preserving server-rendered content.
# Install dependencies
pnpm install
# Start development
pnpm dev
# Run tests
pnpm test
# Build
pnpm build
# Lint
pnpm lint
# Format code
pnpm format- Fork the repository
- Create your feature branch (
git checkout -b feature/amazing-feature) - Commit your changes (
git commit -m 'feat: add amazing feature') - Push to the branch (
git push origin feature/amazing-feature) - Open a Pull Request
MIT