SSR і lazy loading компонентів
Нагадування: що таке SSR і lazy loading
| Механізм | Що робить |
|---|---|
| SSR (Server-Side Rendering) | Генерує HTML на сервері, щоб сторінка відображалася одразу (без очікування JS). |
| Lazy Loading компонентів | Завантажує компонент (або його код) лише за потреби - щоб не тягнути весь бандл одразу. |
Як вони взаємодіють
Коли ти використовуєш React.lazy() або dynamic import() у SSR-застосунку:
- Сервер починає рендерити React-компоненти.
- Якщо зустрічає лінивий компонент, який ще не завантажено (код спліттиться в окремий JS-чанк), сервер не може відрендерити його прямо зараз.
- React:
- або зупиняє рендер (у класичному SSR);
- або показує fallback з
<Suspense>і продовжує стрім (у Streaming SSR); - або дозавантажує модуль синхронно, якщо він уже в пам'яті (у dev-режимі).
1. У класичному SSR (до React 18)
React.lazy() фактично блокує рендеринг, якщо його модуль ще не завантажено.
Наприклад:
const LazyComp = React.lazy(() => import('./BigChart'));
function Page() {
return (
<div>
<Header />
<LazyComp /> {/* SSR тут "застрягне" */}
</div>
);
}На сервері import('./BigChart') - це Promise,
але класичний SSR (renderToString) не вміє чекати проміси.
Результат:
- Сервер видасть помилку: "A component suspended while rendering, but no fallback UI was specified."
- Або просто не зможе завершити SSR.
Тому до React 18 lazy-компоненти не працювали на сервері - їх доводилося вантажити синхронно або відключати SSR для них (ssr: false у Next.js).
2. У React 18 і Streaming SSR
React 18 повністю змінив правила гри. Тепер SSR уміє працювати з Suspense і lazy-компонентами асинхронно.
Що відбувається:
- Сервер починає рендерити HTML.
- Коли React зустрічає лінивий компонент, він "призупиняє" рендер цієї частини (suspend).
- Надсилає в потік HTML для fallback-у (наприклад,
<Skeleton />). - Коли модуль дозавантажиться, React продовжує стримінг і вставляє готовий контент пізніше.
Приклад:
import { Suspense, lazy } from 'react';
const LazyComp = lazy(() => import('./BigChart'));
export default function Page() {
return (
<div>
<Header />
<Suspense fallback={<p>Loading chart...</p>}>
<LazyComp />
</Suspense>
</div>
);
}У Streaming SSR:
<Header />і<p>Loading chart...</p>відрендеряться і одразу відправляться клієнту;- коли
BigChartзавантажиться, сервер дошле решту HTML.
Це робить SSR асинхронним, плавним і "лінивим" по суті.
3. У Next.js
У Next.js (починаючи з версії 13, App Router) це реалізовано з коробки:
import dynamic from 'next/dynamic';
const LazyChart = dynamic(() => import('./Chart'), {
ssr: true, // або false, якщо не хочеш рендерити на сервері
loading: () => <p>Loading...</p>,
});
export default function Page() {
return (
<>
<Header />
<LazyChart />
</>
);
}ssr: true→ компонент буде відрендерений на сервері (через Suspense).ssr: false→ компонент повністю виключається з SSR і рендериться лише на клієнті.
Це дає гнучкість: можна вибрати - "ліниво, але з SSR" або "ліниво, але лише в браузері".
Переваги такої взаємодії
| Перевага | Опис |
|---|---|
| Швидший перший рендер | SSR віддає HTML одразу, навіть якщо частина компонентів вантажиться ліниво |
| Менший JS-бандл | Lazy loading знижує розмір initial bundle |
| Плавне завантаження контенту | Через Suspense SSR віддає fallback, потім замінює на готовий компонент |
| Підтримка асинхронних даних | Lazy-компоненти можуть дозавантажувати дані з API без блокування всього SSR |
Потенційні складнощі
| Проблема | Опис |
|---|---|
Не можна використовувати React.lazy без Suspense | Інакше SSR "зависне" |
| Fallback може залишатися довше очікуваного | Якщо імпорт важкий або мережа повільна |
Старий renderToString не підтримує Suspense | Потрібен renderToPipeableStream() |
| Кешування чанків | Потрібно налаштувати CDN, щоб JS-чанки lazy-компонентів вантажилися коректно |
Приклад повного SSR + lazy loading + Suspense
import { Suspense, lazy } from 'react';
import { renderToPipeableStream } from 'react-dom/server';
const LazySection = lazy(() => import('./Section'));
function App() {
return (
<div>
<Header />
<Suspense fallback={<LoadingSection />}>
<LazySection />
</Suspense>
</div>
);
}
// Сервер
app.get('/', (req, res) => {
const stream = renderToPipeableStream(<App />, {
onShellReady() {
res.setHeader('Content-Type', 'text/html');
stream.pipe(res);
},
});
});Клієнт отримує шапку і LoadingSection одразу,
а Section додається в DOM пізніше, коли готовий.
Висновок
SSR і lazy loading тепер відмінно поєднуються - завдяки React 18 і Streaming SSR:
| Критерій | Старий SSR | Streaming SSR |
|---|---|---|
| Lazy-компоненти працюють? | Ні | Так |
| Suspense підтримується? | Ні | Так |
| Можна віддавати частинами? | Ні | Так |
| UX без "білого екрана"? | Не завжди | Завжди |
Коротка відповідь
Для співбесідиКоротка відповідь допоможе вам впевнено відповідати на цю тему під час співбесіди.