Skip to main content

SSR і lazy loading компонентів

Нагадування: що таке SSR і lazy loading

МеханізмЩо робить
SSR (Server-Side Rendering)Генерує HTML на сервері, щоб сторінка відображалася одразу (без очікування JS).
Lazy Loading компонентівЗавантажує компонент (або його код) лише за потреби - щоб не тягнути весь бандл одразу.

Як вони взаємодіють

Коли ти використовуєш React.lazy() або dynamic import() у SSR-застосунку:

  1. Сервер починає рендерити React-компоненти.
  2. Якщо зустрічає лінивий компонент, який ще не завантажено (код спліттиться в окремий JS-чанк), сервер не може відрендерити його прямо зараз.
  3. React:
  • або зупиняє рендер (у класичному SSR);
  • або показує fallback з <Suspense> і продовжує стрім (у Streaming SSR);
  • або дозавантажує модуль синхронно, якщо він уже в пам'яті (у dev-режимі).

1. У класичному SSR (до React 18)

React.lazy() фактично блокує рендеринг, якщо його модуль ще не завантажено.

Наприклад:

javascript
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-компонентами асинхронно.

Що відбувається:

  1. Сервер починає рендерити HTML.
  2. Коли React зустрічає лінивий компонент, він "призупиняє" рендер цієї частини (suspend).
  3. Надсилає в потік HTML для fallback-у (наприклад, <Skeleton />).
  4. Коли модуль дозавантажиться, React продовжує стримінг і вставляє готовий контент пізніше.

Приклад:

javascript
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) це реалізовано з коробки:

javascript
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

javascript
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:

КритерійСтарий SSRStreaming SSR
Lazy-компоненти працюють?НіТак
Suspense підтримується?НіТак
Можна віддавати частинами?НіТак
UX без "білого екрана"?Не завждиЗавжди

Коротка відповідь

Для співбесіди
Premium

Коротка відповідь допоможе вам впевнено відповідати на цю тему під час співбесіди.