Progressive Hydration — Hydrating Components Incrementally for Better Performance
In this tutorial, you will learn about Progressive Hydration. We cover key concepts, practical examples, and best practices to help you master this topic.
Progressive hydration hydrates visible interactive components first, deferring non-critical hydration to improve Time to Interactive and perceived performance.
What You'll Learn
By the end of this tutorial, you will understand what progressive hydration is, how it improves performance by prioritizing visible and interactive components, how to implement progressive hydration patterns, and how it differs from selective and partial hydration.
Why It Matters
Traditional SSR hydrates the entire component tree at once. For large pages, this blocks the main thread for seconds. Users see content but cannot click buttons or interact. Progressive hydration prioritizes visible, interactive components first, letting users interact with the page while less important sections load in the background.
Real-World Use
A booking site with 50+ widgets on the homepage used progressive hydration. The search form and date picker hydrated immediately. The customer reviews, related destinations, and footer widgets hydrated progressively. Time to Interactive dropped from 4.5s to 1.2s. Users could search for bookings while less critical widgets continued loading.
Progressive Hydration Priority
┌──────────────────────────────────────────────────────────┐
│ Progressive Hydration — Priority Order │
├──────────────────────────────────────────────────────────┤
│ │
│ Immediate (0ms): │
│ ┌────────────────────────────────────────────────┐ │
│ │ Search form, Navigation, Primary CTA │ │
│ │ (User visible and interactive — highest priority)│ │
│ └────────────────────────────────────────────────┘ │
│ │
│ Soon (500ms): │
│ ┌────────────────────────────────────────────────┐ │
│ │ Below-fold widgets, Secondary content │ │
│ │ (Visible but not immediately interactive) │ │
│ └────────────────────────────────────────────────┘ │
│ │
│ Deferred (3-5s): │
│ ┌────────────────────────────────────────────────┐ │
│ │ Footer, Analytics, Chat widget │ │
│ │ (Not in viewport, background tasks) │ │
│ └────────────────────────────────────────────────┘ │
│ │
│ On Interaction: │
│ ┌────────────────────────────────────────────────┐ │
│ │ Modal, Dropdown, Tooltip │ │
│ │ (Only hydrate when user interacts) │ │
│ └────────────────────────────────────────────────┘ │
│ │
└──────────────────────────────────────────────────────────┘
Think of progressive hydration like a restaurant that starts cooking popular dishes first. The most-ordered items (navigation, search) are prepared immediately. Less popular items (footer, sidebar) are prepared when the kitchen has capacity. Items that few people order (modal, tooltip) are prepared only when someone places that specific order.
Progressive Hydration with requestIdleCallback
import { useState, useEffect, useRef } from 'react';
// Hook for progressive hydration
function useProgressiveHydration(options = {}) {
const {
priority = 'low', // 'critical', 'high', 'low', 'onInteraction'
idleTimeout = 3000, // Max time to wait for idle
} = options;
const [isHydrated, setIsHydrated] = useState(false);
const [hasInteracted, setHasInteracted] = useState(false);
const elementRef = useRef(null);
useEffect(() => {
if (priority === 'onInteraction') {
// Wait for user interaction (hover, focus, click)
const element = elementRef.current;
if (!element) return;
const handler = () => {
setHasInteracted(true);
setIsHydrated(true);
};
element.addEventListener('mouseenter', handler, { once: true });
element.addEventListener('focusin', handler, { once: true });
return () => {
element.removeEventListener('mouseenter', handler);
element.removeEventListener('focusin', handler);
};
}
if (priority === 'critical') {
// Hydrate immediately
setIsHydrated(true);
return;
}
// For 'high' and 'low' priority, use requestIdleCallback
const idleCallbackId = requestIdleCallback(
() => setIsHydrated(true),
{ timeout: idleTimeout }
);
return () => cancelIdleCallback(idleCallbackId);
}, [priority, idleTimeout]);
return { isHydrated, ref: elementRef };
}
// Usage in components
function SearchForm() {
// Critical — hydrate immediately
const { isHydrated, ref } = useProgressiveHydration({ priority: 'critical' });
if (!isHydrated) {
return <div className="search-placeholder" />;
}
return (
<form ref={ref} onSubmit={handleSearch}>
<input type="search" placeholder="Search..." />
<button type="submit">Search</button>
</form>
);
}
function FooterWidget() {
// Low priority — hydrate during idle time
const { isHydrated } = useProgressiveHydration({ priority: 'low' });
if (!isHydrated) {
return <div className="widget-skeleton" />;
}
return <InteractiveFooterContent />;
}
Progressive Hydration with Intersection Observer
import { useState, useEffect, useRef } from 'react';
// Hydrate only when component is visible
function useVisibilityHydration(options = {}) {
const {
rootMargin = '200px', // Start loading 200px before visible
once = true
} = options;
const [isVisible, setIsVisible] = useState(false);
const ref = useRef(null);
useEffect(() => {
const element = ref.current;
if (!element) return;
const observer = new IntersectionObserver(
([entry]) => {
if (entry.isIntersecting) {
setIsVisible(true);
if (once) {
observer.unobserve(element);
}
}
},
{ rootMargin }
);
observer.observe(element);
return () => observer.disconnect();
}, [rootMargin, once]);
return { isVisible, ref };
}
// Below-fold component
function CustomerReviews() {
// Hydrate when within 200px of viewport
const { isVisible, ref } = useVisibilityHydration({ rootMargin: '200px' });
return (
<section ref={ref}>
{isVisible ? (
<InteractiveReviews />
) : (
<div className="reviews-placeholder" />
)}
</section>
);
}
// Hydrate only on interaction
function ChatWidget() {
const [isOpen, setIsOpen] = useState(false);
if (!isOpen) {
return (
<button onClick={() => setIsOpen(true)}>
Open Chat
</button>
);
}
// Chat component is loaded and hydrated on demand
return <ChatComponent />;
}
Common Mistakes
- Hydrating too aggressively. Hydrating everything immediately defeats the purpose of progressive hydration. Be intentional about which components need immediate interactivity.
- Hydrating too late. Critical interactive components (search, navigation, forms) must hydrate immediately. Delaying their hydration frustrates users who try to interact.
- Not measuring hydration impact. Use Chrome DevToolsk "DevTools" >}} Performance tab to measure Time to Interactive. Compare before and after progressive hydration.
- Forgetting about Accessibility. Progressive hydration should not break keyboard navigation or screen reader support. Ensure focus management works correctly.
- Complex hydration logic slowing down critical path. The hydration prioritization logic itself should not block the main thread. Use lightweight checks.
Practice Questions
- What is the difference between progressive hydration and traditional hydration?
- How does requestIdleCallback help with progressive hydration?
- How does Intersection Observer help with lazy hydration?
- What components should hydrate immediately vs deferred?
- How do you measure the impact of progressive hydration?
Challenge: Build a page with 5 components at different hydration priorities: search form (critical, immediately), image gallery (high, after critical hydration), article comments (normal, idle time), related articles (low, after idle), and share buttons (on interaction). Measure TTI with and without progressive hydration.
FAQ
Mini Project
Build a news homepage with progressive hydration: navigation and search (critical), article list (high), sidebar widgets (low), footer newsletter signup (idle), and share buttons (on interaction). Measure TTI and FID with and without progressive hydration using Chrome DevTools.
What's Next
You understand progressive hydration. Now explore Selective Hydration to understand React 18's selective hydration approach.
Built by the developers of DodaTech
Doda Browser, DodaZIP & Durga Antivirus Pro