Реалізація Partial Hydration для оптимізації веб-застосунку
Повна гідрація (Full Hydration) — коли браузер завантажує весь JS-бандл застосунку та оживляє весь DOM, включаючи компоненти, які ніколи не стануть інтерактивними. Статичний заголовок, список статей, футер — все гідрується. Прямі втрати: зайвий JS, зайві CPU, повільний TTI.
Partial Hydration — гідрація тільки тих компонентів, які їй потребують. Статичний контент залишається інертним HTML. JS завантажується тільки для інтерактивних островків.
Проблема Full Hydration
Типова сторінка блогу:
- Header (статичний) → гідрувати? Навіщо?
- Navigation (статичний) → гідрувати? Навіщо?
- ArticleContent (MDX) → гідрувати? Навіщо?
- Comments (інтерактивні) → ПОТРІБНА гідрація
- ShareButtons (onClick) → ПОТРІБНА гідрація
- Footer (статичний) → гідрувати? Навіщо?
Без partial hydration: завантажується весь React (~45 KB) + весь код компонентів
З partial hydration: завантажується тільки код Comments + ShareButtons
Реалізація в Astro (Islands Architecture)
Astro реалізує partial hydration через директиви client:*:
---
// src/pages/article/[slug].astro
import ArticleHeader from '@/components/ArticleHeader.astro'; // Серверний
import ArticleContent from '@/components/ArticleContent.astro'; // Серверний
import CommentSection from '@/components/CommentSection.tsx'; // React, потребує гідрації
import ShareWidget from '@/components/ShareWidget.svelte'; // Svelte, потребує гідрації
import NewsletterForm from '@/components/NewsletterForm.vue'; // Vue, потребує гідрації
const { slug } = Astro.params;
const article = await getArticle(slug);
---
<html>
<body>
<!-- Нулевий JS — чистий HTML -->
<ArticleHeader title={article.title} author={article.author} />
<ArticleContent content={article.content} />
<!-- Гідрація при появленні у viewport -->
<CommentSection articleId={article.id} client:visible />
<!-- Гідрація при першому взаємодії -->
<ShareWidget url={Astro.url.href} client:idle />
<!-- Негайна гідрація (критичний контент) -->
<NewsletterForm client:load />
</body>
</html>
Директиви гідрації:
| Директива | Коли завантажується JS |
|---|---|
client:load |
Одразу при завантаженні сторінки |
client:idle |
Після requestIdleCallback (браузер не займаний) |
client:visible |
Коли компонент входить у viewport |
client:media="..." |
При совпаданні медіа-запиту |
client:only="react" |
Тільки на клієнті, без SSR |
Реалізація в Next.js через динамічний імпорт
import dynamic from 'next/dynamic';
// Ці компоненти НЕ включаються у initial bundle
const CommentSection = dynamic(() => import('@/components/CommentSection'), {
ssr: false,
loading: () => <CommentsSkeleton />,
});
const VideoPlayer = dynamic(() => import('@/components/VideoPlayer'), {
ssr: false, // Немає сенсу рендерити на сервері
});
const HeavyChart = dynamic(() => import('@/components/analytics/HeavyChart'), {
ssr: false,
loading: () => <div className="h-64 animate-pulse bg-gray-100 rounded" />,
});
// Гідрація при появленні у viewport — нативний IntersectionObserver
function LazyHydrate({ children, rootMargin = '200px' }) {
const [hydrated, setHydrated] = useState(false);
const ref = useRef<HTMLDivElement>(null);
useEffect(() => {
const observer = new IntersectionObserver(
([entry]) => { if (entry.isIntersecting) setHydrated(true); },
{ rootMargin }
);
if (ref.current) observer.observe(ref.current);
return () => observer.disconnect();
}, []);
return <div ref={ref}>{hydrated ? children : <div style={{ minHeight: '1px' }} />}</div>;
}
// Використання
export default function ArticlePage({ article }) {
return (
<article>
<ArticleHeader article={article} /> {/* Серверний, 0 JS */}
<ArticleBody content={article.content} /> {/* Серверний, 0 JS */}
<LazyHydrate>
<CommentSection articleId={article.id} />
</LazyHydrate>
</article>
);
}
Прогресивна гідрація
Гідруйте компоненти послідовно, не блокуючи main thread:
'use client';
import { useEffect, useState } from 'react';
// Планувальник гідрації через requestIdleCallback
function useIdleHydration(delay = 0): boolean {
const [ready, setReady] = useState(false);
useEffect(() => {
let id: number;
if ('requestIdleCallback' in window) {
id = requestIdleCallback(() => setReady(true), { timeout: delay || 2000 });
} else {
id = setTimeout(() => setReady(true), delay) as unknown as number;
}
return () => {
'requestIdleCallback' in window
? cancelIdleCallback(id)
: clearTimeout(id);
};
}, [delay]);
return ready;
}
function IdleComponent({ children, fallback }: IdleProps) {
const ready = useIdleHydration(1000);
return ready ? children : fallback;
}
Вимірювання ефекту
Інструменти для оцінки до та після partial hydration:
# Webpack Bundle Analyzer
npx @next/bundle-analyzer
# Astro Check: які компоненти додають JS
npx astro check
# Lighthouse CLI для автоматизації
npx lighthouse https://example.com --output json \
--only-categories=performance \
| jq '.categories.performance.score'
Метрики, які змінюються:
| Метрика | Очікуване поліпшення |
|---|---|
| Total Blocking Time (TBT) | -40–70% |
| Time to Interactive (TTI) | -30–60% |
| JS Parse/Execute час | -50–80% |
| Lighthouse Performance Score | +10–25 балів |
Коли partial hydration недоцільна
Partial Hydration не має сенсу для:
- SPA з багатою інтерактивністю на кожній сторінці
- Застосунків, де майже всі компоненти клієнтські
- Дашбордів за авторизацією — SEO не важливий, JS завантажується один раз
Максимальна користь на публічних контентних сторінках: блоги, документація, каталоги, лендинги.
Строки реалізації
- Тиждень 1: аудит JS-бандла, виявлення компонентів без клієнтської логіки, профілювання TTI
-
Тиждень 2–3: розмітка компонентів на серверні/клієнтські, внедрення директив (
client:visible,client:idle) абоdynamic()зssr:false - Тиждень 4: LazyHydrate обгортки для контенту нижче фолду, вимірювання метрик
- Тиждень 5: регресійне тестування, Lighthouse CI у конвеєрі, документація







