Skip to content
PПромтбук
RUEN
04Производительность

Стратегия 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 viewportFooter, "related posts", графики ниже фолда, секция отзывовIntersectionObserver
Lazy by interactionМодалки, drawer, autocomplete, share-popup, video-playerOn 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/desktopclient: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 уехал
  • ❌ Динамический импорт без loading skeleton — 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
Похожие промты