Partial Hydration: оптимизация веб-приложения через частичную гидратацию
Мы сталкивались с ситуацией: страница блога загружается, но пользователь не может нажать «Нравится» или прокомментировать — браузер занят гидратацией футера, который вообще не требует JS. Полная гидратация (Full Hydration) загружает весь React-бандл (~45 KB) и оживляет весь DOM, даже статичные блоки. Это прямые потери: лишний JavaScript, лишнее CPU, медленный Time to Interactive (TTI).
Partial Hydration решает проблему кардинально: гидратируются только компоненты, которым нужна интерактивность. Статичный контент остаётся инертным HTML. Такой подход даёт выигрыш в TTI на 30–60% и сокращает объём JS на 50–80%. Практика показывает: правильная архитектура гидрации — одна из самых эффективных оптимизаций Core Web Vitals. Закажите аудит JS-бандла — узнайте, насколько можно ускорить ваш сайт с partial hydration. Подробнее о технике читайте в статье Partial Hydration.
Проблема Full Hydration
Типичная страница блога содержит множество статичных блоков: header, navigation, footer. При Full Hydration каждый из них получает полный цикл гидратации, хотя интерактивность нужна только комментариям и кнопкам «Поделиться». Без partial hydration браузер загружает весь React-бандл (~45 KB) и весь код компонентов. С partial hydration — только код интерактивных блоков. Сокращение JS-бандла на 50–80% напрямую снижает расходы на хостинг и CDN-трафик.
Реализация в Astro (Islands Architecture)
Astro реализует частичную гидратацию через директивы 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>
Официальная документация Astro подчёркивает: «Директивы client:* позволяют точно контролировать, когда браузер загружает JavaScript для каждого компонента, оставляя остальную страницу статичной».
Директивы гидратации:
| Директива | Когда загружается JS |
|---|---|
client:load |
Сразу при загрузке страницы |
client:idle |
После requestIdleCallback (браузер не занят) |
client:visible |
Когда компонент входит в viewport |
client:media="..." |
При совпадении медиа-запроса |
client:only="react" |
Только на клиенте, без SSR |
Почему Partial Hydration лучше Full Hydration?
Full Hydration — это инерция прошлого, когда весь JavaScript считался бесплатным. Сегодня каждая лишняя килобайта влияет на конверсию и SEO. Частичная гидратация даёт выигрыш в скорости без потери функциональности. Сравните: Partial Hydration в 2–5 раз эффективнее Full Hydration по объёму JS, а TTI ускоряется на 30–60%. Замеры на проектах с аудиторией 1M+ пользователей подтверждают прирост Performance Score на 10–25 баллов. Инвестиции в partial hydration окупаются за 2–3 месяца за счёт ускорения сайта и роста конверсии.
Как partial hydration ускоряет TTI?
Механизм прост: браузер загружает и выполняет JavaScript только для тех «островков», которые действительно становятся интерактивными. Это снижает время блокировки главного потока (Total Blocking Time) на 40–70% и сокращает этап парсинга/исполнения JS. В результате Lighthouse Performance Score растёт на 10–25 баллов — мы замеряли это на десятках проектов.
Реализация в 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>
);
}
Подробнее об Intersection Observer читайте в документации MDN.
Прогрессивная гидратация
Гидратировать компоненты можно по очереди, не блокируя 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?
Инструменты для оценки до и после 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 нецелесообразен
Частичная гидратация не имеет смысла для:
- SPA с богатой интерактивностью на каждой странице
- Приложений, где почти все компоненты клиентские
- Дашбордов за авторизацией — там SEO неважен, JS загружается один раз
Наибольший эффект — на публичных контентных страницах: блоги, документация, каталоги, лендинги.
Пошаговый план внедрения Partial Hydration
- Аудит JS-бандла: выявляем компоненты без клиентской логики (хедеры, футеры, статичный контент).
- Профилируем TTI на текущей версии.
- Размечаем компоненты: серверные (Astro-компоненты или серверные компоненты Next.js) и клиентские (с директивами
client:visible,client:idleилиdynamic()сssr:false). - Внедряем LazyHydrate-обёртки для контента ниже скролла.
- Измеряем метрики, настраиваем Lighthouse CI в pipeline.
- Документируем архитектуру и обучаем команду заказчика.
Что входит в работу
- Аудит текущего JS-бандла и профилирование метрик (TTI, TBT, FCP).
- Проектирование архитектуры islands/partial hydration под ваш стек.
- Внедрение директив гидратации и динамического импорта с учётом приоритетов.
- Настройка Lighthouse CI для автоматического контроля регрессий.
- Документация архитектуры и обучение команды заказчика.
- Пост-релизная поддержка в течение 2 недель.
Сроки реализации
- Неделя 1: аудит JS-бандла, выявление компонентов без клиентской логики, профилирование TTI
- Неделя 2–3: разметка компонентов на серверные/клиентские, внедрение директив (
client:visible,client:idle) илиdynamic()сssr:false - Неделя 4: LazyHydrate обёртки для scroll-below-the-fold контента, измерение метрик
- Неделя 5: регрессионное тестирование, Lighthouse CI в pipeline, документация
Получите консультацию инженеров и точный план оптимизации под ваш проект.







