← Назад к вопросам
Как решается проблема загрузки интерактива на Server-Side Rendering?
2.0 Middle🔥 151 комментариев
#Soft Skills и рабочие процессы
Комментарии (1)
🐱
claude-haiku-4.5PrepBro AI3 апр. 2026 г.(ред.)
Ответ сгенерирован нейросетью и может содержать ошибки
Гидрация и SSR: решение проблемы интерактивности
Это ключевая проблема в современном фронтенде. Многие разработчики путаются между SSR, гидрацией и CSR. Разберусь подробно с примерами.
1. Что такое проблема SSR с интерактивом
Проблема: браузер получает HTML с сервера, но он неинтерактивен. Пользователь видит контент, но не может на него кликать.
<!-- Сервер отрендерил HTML -->
<div>
<h1>Hello World</h1>
<button>Click me</button> <!-- Выглядит как кнопка, но не работает! -->
</div>
<!-- JS ещё не загрузился -->
<!-- onClick handler не прикреплён -->
Решение: гидрация (hydration) - процесс прикрепления обработчиков событий к уже существующему HTML.
2. Как работает гидрация в Next.js
// server.js - Server-Side Rendering
import ReactDOMServer from "react-dom/server";
function App() {
return <button onClick={() => alert("Clicked")}>Click me</button>;
}
const html = ReactDOMServer.renderToString(<App />);
// Результат: <button>Click me</button>
// Но onClick НЕ прикреплён!
// client.js - Гидрация
import ReactDOM from "react-dom/client";
function App() {
return <button onClick={() => alert("Clicked")}>Click me</button>;
}
// Гидрируем существующий DOM
const root = ReactDOM.hydrateRoot(document.getElementById("root"), <App />);
// Теперь обработчик события прикреплён к существующей кнопке
Ключевая разница:
ReactDOM.render()- удаляет старый DOM и заменяет новымReactDOM.hydrateRoot()- реиспользует существующий DOM и добавляет обработчики
3. Next.js - полный пример
// pages/index.tsx - автоматический SSR + гидрация
import { useState } from "react";
export default function Home() {
const [count, setCount] = useState(0);
return (
<div>
<h1>Count: {count}</h1>
<button onClick={() => setCount(count + 1)}>
Increment
</button>
</div>
);
}
// Next.js автоматически:
// 1. Рендерит на сервере -> HTML
// 2. Отправляет HTML клиенту
// 3. Клиент гидрирует компонент
// 4. Теперь компонент интерактивный!
Что происходит:
1. Сервер отрендерил:
<div><h1>Count: 0</h1><button>Increment</button></div>
2. Клиент получил HTML (быстро отображается)
3. JavaScript загрузился и гидрировал компонент
4. Теперь onClick работает
4. Проблемы гидрации
Проблема 1: Mismatch между сервером и клиентом
// Это вызовет ошибку!
export default function App() {
const [isMounted, setIsMounted] = useState(false);
useEffect(() => {
setIsMounted(true);
}, []);
// На сервере: false
// На клиенте до гидрации: false
// На клиенте после гидрации: true
return <div>{isMounted ? "Client" : "Server"}</div>;
}
// Ошибка: "Hydration mismatch: Expected server HTML to contain a matching..."
Решение: Убедиться, что сервер и клиент рендерят одно и то же.
// Правильно
export default function App() {
// НЕ используем состояние, которое меняется после монтирования
const timestamp = new Date().getTime(); // НЕПРАВИЛЬНО!
return <div>{timestamp}</div>;
}
// ПРАВИЛЬНО - используем статичные значения
export default function App() {
return <div>Hello World</div>;
}
// Или используем флаг для отложенного рендера
export default function App() {
const [isClient, setIsClient] = useState(false);
useEffect(() => {
setIsClient(true);
}, []);
return (
<div>
{isClient ? <ClientOnlyComponent /> : null}
</div>
);
}
Проблема 2: Время гидрации (TTI - Time to Interactive)
// Плохо - большой JavaScript файл = долгая гидрация
export default function App() {
const largeArray = Array.from({ length: 1000000 }, (_, i) => i);
// Гидрация может занять 5+ секунд!
return <div>{largeArray.length}</div>;
}
// Хорошо - ленивая загрузка
import dynamic from "next/dynamic";
const HeavyComponent = dynamic(() => import("./Heavy"), {
loading: () => <div>Loading...</div>,
ssr: false // Загрузить только на клиенте
});
export default function App() {
return (
<div>
<Suspense fallback={<div>Loading</div>}>
<HeavyComponent />
</Suspense>
</div>
);
}
5. React 18 - Streaming и Suspense
// Новый подход с React 18 Streaming
import { Suspense } from "react";
import StreamingComponent from "./StreamingComponent";
export default function App() {
return (
<div>
<Header /> {/* Отправляется сразу */}
<Suspense fallback={<Skeleton />}>
<SlowComponent /> {/* Отправляется позже */}
</Suspense>
</div>
);
}
// На сервере:
// 1. Отправляет Header + Skeleton
// 2. Клиент может начать гидрировать Header
// 3. Позже отправляет SlowComponent
// 4. Гидрирует SlowComponent
// Это быстрее, чем ждать всего контента!
6. Progressive Hydration - Гидрирование по частям
// pages/_app.tsx
import Script from "next/script";
export default function App({ Component, pageProps }) {
return (
<>
<Component {...pageProps} />
{/* Гидрируем критичные компоненты первыми */}
<Script src="/hydrate-critical.js" strategy="beforeInteractive" />
{/* Остальные после взаимодействия */}
<Script src="/hydrate-rest.js" strategy="afterInteractive" />
</>
);
}
// Результат:
// 1. Пользователь видит контент (SSR HTML)
// 2. Критичные компоненты гидрируются первыми (формы, навигация)
// 3. Остальное гидрируется позже (комментарии, реклама)
7. Islands Architecture - Острова интерактивности
// Новый подход: только некоторые компоненты интерактивны
export default function Page() {
return (
<>
{/* Статичный HTML - НЕ гидрируется */}
<Header title="My Blog" />
<div className="content">
Static content here...
</div>
{/* Интерактивный остров - гидрируется */}
<CommentSection />
{/* Ещё одно статичное содержимое */}
<Footer />
</>
);
}
// Это быстрее, так как гидрируется только то, что нужно!
8. Практический пример: Full Stack Next.js
// app/page.tsx
"use client"; // Boundary для клиентского кода
import { useState, useEffect } from "react";
export default function Home() {
const [data, setData] = useState(null);
const [isClient, setIsClient] = useState(false);
useEffect(() => {
setIsClient(true);
// Загрузить данные с клиента, если нужны свежие
}, []);
// На сервере: null + false
// На клиенте после гидрации: null + true
// После эффекта: data + true
return (
<div>
{isClient ? (
<div>Client-side content: {data}</div>
) : (
<div>Server-side content</div>
)}
</div>
);
}
9. Производительность - что измерять
// Web Vitals для SSR
// - FCP (First Contentful Paint) - видит контент
// - LCP (Largest Contentful Paint) - основной контент загружен
// - TTI (Time to Interactive) - можно кликать
// - FID (First Input Delay) - задержка перед ответом
// С хорошей SSR + гидрацией:
// FCP: 1s (сервер быстро отправляет HTML)
// LCP: 2s (критичный контент видим)
// TTI: 3s (можно взаимодействовать)
// Без SSR (только CSR):
// FCP: 3s (ждём JavaScript)
// LCP: 4s
// TTI: 5s
10. Чек-лист для собеседования
// 1. Что такое гидрация?
// Ответ: Процесс прикрепления обработчиков к существующему DOM
// 2. В чём разница между render и hydrateRoot?
// Ответ: render создаёт новый DOM, hydrateRoot переиспользует существующий
// 3. Что такое hydration mismatch?
// Ответ: Когда HTML на сервере отличается от того, что рендерит клиент
// 4. Как быстрее: SSR или CSR?
// Ответ: SSR быстрее для First Paint, но TTI может быть медленнее
// 5. Что такое Streaming SSR?
// Ответ: Отправляем HTML по частям, не ждём всего контента
Вывод
SSR + гидрация - это лучшее из двух миров:
- SSR часть: быстрый First Paint, SEO, доступность
- Гидрация: преобразует статичный HTML в интерактивное приложение
- Progressive Hydration: оптимизирует TTI, гидрируя по необходимости
Используй Next.js 13+ с App Router - там всё уже оптимизировано.