← Назад к вопросам

Как решается проблема загрузки интерактива на 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 - там всё уже оптимизировано.

Как решается проблема загрузки интерактива на Server-Side Rendering? | PrepBro