Стратегия lazy hydration
Что гидратировать сразу, что лениво, по чему триггерить — для Astro islands, RSC, Vue suspense.
Спроектируй стратегию hydration для {{page}} в {{framework}}.
Базовая аксиома: hydration — самый дорогой шаг на клиенте. Это парс JS + повторное построение дерева компонентов + навешивание listener'ов. Если гидратировать всё сразу — TBT и INP в полу. Цель: гидратировать минимум, отложить остальное до того момента, когда оно реально нужно.
1. Карта страницы — три ведра
Раздели каждый компонент на:
| Ведро | Что туда | Стратегия |
|---|---|---|
| Eager | Нужно интерактивно сразу: header-меню, поисковая строка в хедере, hero CTA, AB-флаги | Hydrate at mount |
| Lazy by viewport | Footer, "related posts", графики ниже фолда, секция отзывов | IntersectionObserver |
| Lazy by interaction | Модалки, drawer, autocomplete, share-popup, video-player | On click/focus/hover |
| Never (static) | Заголовок, статичный текст, картинка-плейсхолдер, иконка | Не гидрировать вообще |
Главная ошибка: класть всё в eager «на всякий». Footer не нужен интерактивным до того, как пользователь до него доскроллит.
2. Frameworks — нативные механизмы
Astro (islands architecture)
<Header client:load /> {/* eager */}
<Hero client:idle /> {/* после requestIdleCallback */}
<Chart client:visible /> {/* при попадании в viewport */}
<Modal client:only="react" /> {/* skip SSR, client-only */}
<StaticBio /> {/* zero JS */}
Директивы: load | idle | visible | media={query} | only. По умолчанию — zero JS.
Next.js App Router (React Server Components)
// Server Component — zero client JS by default
export default function Page() {
return (
<>
<StaticHero />
<ClientHeader /> {/* "use client" */}
<Suspense fallback={<Skel/>}>
<ChartClient /> {/* lazy split + suspense */}
</Suspense>
</>
);
}
// app/components/ChartClient.tsx
'use client';
import dynamic from 'next/dynamic';
const Chart = dynamic(() => import('./Chart'), { ssr: false, loading: () => <Skel/> });
Главный приём: компоненты — Server по умолчанию, 'use client' только где нужно интерактив. Не объявляй 'use client' на root.
Vue / Nuxt
<LazyHeavyChart /> <!-- Nuxt 3: auto split + lazy import -->
<ClientOnly><Modal/></ClientOnly>
<NuxtIsland name="Hero" /> <!-- server component, no client JS -->
Плюс <Suspense> для async setup.
SvelteKit
По умолчанию hydrate page-level. Сегментный: <svelte:component this={Lazy}/> + dynamic import.
Qwik
Resumability вместо hydration — ничего не гидратируется заранее, event handlers сериализуются.
3. Триггеры — какой когда
| Триггер | Используй для | API |
|---|---|---|
| mount | Только то, что должно работать на первой шкале экрана | client:load / default |
| idle | Не критичное, но нужное скоро (analytics widget) | requestIdleCallback / client:idle |
| visible | Ниже фолда, тяжёлое (charts, maps) | IntersectionObserver / client:visible |
| interaction | Реактивно только при действии (modal, dropdown) | event listener → import |
| media | Разные стратегии mobile/desktop | client:media="(min-width: 768px)" |
| route prefetch | Перед навигацией на ховер ссылки | <Link prefetch> |
4. Hover/focus prefetch
Самый быстрый воспринимаемый клик:
<button
onMouseEnter={() => import('./HeavyModal')}
onFocus={() => import('./HeavyModal')}
onClick={openModal}
>
Импорт стартует за 100–300мс до клика → к моменту клика чанк уже в кеше.
5. SSR vs CSR — кого вообще не SSR
Не SSR:
- Чисто клиентские виджеты, ломающиеся на сервере (Window-зависимые)
- Контент, зависящий от auth state, кеш которого вреден
- A/B-варианты на критичных секциях
Не CSR:
- Контент для SEO (статьи, продуктовые карточки)
- Above-the-fold, чтобы LCP не упирался в JS
6. Замер импакта
Перед / после на одних и тех же условиях (cold cache, throttled CPU 4x, Slow 3G):
| Метрика | Цель |
|---|---|
| TBT | -50% |
| INP | < 200ms |
Hydration time (RUM или PerformanceObserver) | -40-70% |
| Initial JS gzip | -100kb+ |
Web Vitals в проде: web-vitals библиотека + аналитика → реальные числа.
7. Анти-паттерны конкретно про hydration
- ❌
'use client'на root layout — весь tree становится клиентским - ❌
client:loadна каждый Astro-компонент — теряешь смысл islands - ❌ Lazy hero — fold пустой пока JS не подгрузится, LCP уехал
- ❌ Динамический импорт без
loadingskeleton — CLS прыгает - ❌
ssr: falseна SEO-критичной секции — пустота для бота - ❌
IntersectionObserverбезrootMargin— гидрация ровно когда блок виден, опоздание на 200мс - ❌ Гидратация модалки на mount — 30kb JS впустую, юзер модалку не откроет
- ❌ Прокидывание server-only данных в client component через props без сериализации — runtime ошибка
- ❌ Lazy всё подряд → INP хуже от десятков chunk-fetch при первом скролле
- ❌ Хардкод
windowв server component — крах SSR - ❌ Отсутствие prefetch на hover ссылок главного меню — клик ощущается медленным
8. Что отдать
- Карта компонентов: имя → ведро (eager / viewport / interaction / static)
- Конкретные правки (директивы /
dynamic/'use client'границы) - Список prefetch-целей (hover ссылок, route prefetch)
- Замер до/после (TBT, INP, hydration time, initial JS)
- Чек-лист: ни один SEO-критичный блок не
ssr: false, ни один above-fold не lazy
Аудит производительности (Core Web Vitals)
Глубокая проверка LCP, INP, CLS с привязкой к коду и приоритизированным планом исправлений.
Мастер-аудит сайта: 6 измерений за один проход
Orchestrator-аудит по 6 направлениям: UX, accessibility, performance, SEO, brand consistency, security. Quick scan + deep dive + приоритизированный план + композитная оценка + roadmap.
Performance budget по типам страниц
Бюджеты JS/CSS/images для разных типов страниц, целевые Web Vitals, enforcement в CI с конкретными порогами.