Запропонувати правкуПокращити цю статтюДопрацюйте відповідь до «Як працює серверна рендеринг (SSR) у Next.js». Ваші зміни проходять модерацію перед публікацією.Потрібне підтвердженняКонтентЩо ви змінюєте🇺🇸EN🇺🇦UAПереглядЗаголовок (UA)Коротка відповідь (UA)**SSR (server-side rendering, серверний рендеринг)** у Next.js генерує повний HTML на сервері для кожного запиту, використовуючи актуальні дані, перш ніж надіслати результат браузеру для React-гідратації. ```tsx const res = await fetch('/api/data', { cache: 'no-store' }); ``` **Головне:** браузер отримує реальний контент у першій відповіді, без очікування поки JavaScript завантажиться і зробить запити.Показується над повною відповіддю для швидкого нагадування.Відповідь (UA)Зображення**SSR (server-side rendering, серверний рендеринг)** у Next.js генерує повний HTML на сервері для кожного вхідного запиту, використовуючи свіжі дані, і лише потім надсилає результат браузеру. ## Теорія ### TL;DR - SSR як ресторанна кухня: готує страву щоразу за новим замовленням. SSG заздалегідь готує пачку і роздає. - Кожен запит запускає отримання свіжих даних і повний рендер HTML на сервері. - В App Router достатньо `cache: 'no-store'` у fetch або викликати `cookies()` / `headers()` - Next.js сам переводить сторінку в SSR-режим. - SSR - для персональних або змінних даних. Все, що можна кешувати, краще обслуговувати через SSG або ISR. ### Швидкий приклад ```tsx // app/dashboard/page.tsx async function Dashboard() { const res = await fetch('https://api.example.com/user-data', { cache: 'no-store' // свіжі дані на кожен запит }); const data = await res.json(); return <div>Баланс: {data.balance} грн</div>; } ``` На кожен запит Next.js виконує цю функцію на сервері, отримує актуальні дані, рендерить `<div>Баланс: 150 грн</div>` і надсилає готовий HTML браузеру. JavaScript завантажується пізніше - React гідратує сторінку і підключає обробники подій. ### Головна різниця SSR відрізняється від SSG одним: моментом генерації. SSG будує HTML раз під час деплою і віддає з CDN миттєво. SSR будує HTML під кожен запит на сервері. Перший байт приходить повільніше, але дані завжди актуальні. Порівняно з CSR (рендеринг на клієнті) - SSR надсилає реальний контент у початковому HTML. Пошукові боти бачать його одразу, а користувач отримує змістовний перший екран без очікування JavaScript. ### Коли використовувати - **Персональні дані** - профіль, налаштування, дашборд зі session-даними, все що за логіном. - **Дані в реальному часі** - ціни акцій, рахунки матчів, залишки товарів, де важлива свіжість. - **Контент залежить від авторизації** - сторінки, що читають cookies або заголовки щоб вирішити що показати. - **Статичний або рідко змінний контент** - обери SSG, він швидший і не навантажує сервер. - **Великий трафік, дані можуть бути на хвилину старішими** - ISR з `revalidate` підходить краще. ### Таблиця порівняння | Характеристика | SSR | SSG | CSR | |---|---|---|---| | Дані отримуються | На кожен запит (сервер) | Під час збірки | Після завантаження (клієнт) | | Початковий HTML | Повністю готовий | Пре-зібраний статичний файл | Порожня оболонка | | TTFB | Вищий (обчислення на сервері) | Найнижчий (CDN) | Низький, але без контенту | | SEO | Відмінне | Відмінне | Погане без pre-rendering | | Навантаження на сервер | На кожен запит | Відсутнє після збірки | Низьке | | Найкраще для | Унікальний / живий контент | Маркетинг, документація | App-подібні дашборди після логіну | ### Як це працює всередині Запит потрапляє на Node.js-сервер. Next.js визначає сторінку як динамічну - через `cache: 'no-store'`, виклик `cookies()`, `headers()` або `export const dynamic = 'force-dynamic'`. Запускається async server component, отримує дані через HTTP або напряму з БД. React перетворює дерево компонентів у HTML-рядок через `ReactDOMServer.renderToString()`. Цей HTML вирушає в браузер. Браузер завантажує JS-бандли, потім `ReactDOM.hydrateRoot()` підключає обробники подій до вже існуючого DOM без повторного рендеру. Streaming змінює картину: з React 18 і App Router Next.js надсилає HTML-оболонку одразу, а чанки підтягуються по мірі того як кожна `Suspense`-межа резолвиться. Повільний API не гальмує всю сторінку. ### Типові помилки **Виклик `cookies()` у клієнтському компоненті** ```tsx // Неправильно - викидає "Headers cannot be used in Client Components" 'use client'; import { cookies } from 'next/headers'; const session = cookies().get('session'); // помилка // Правильно - читай cookies у серверному компоненті, передавай через props async function ServerLayout() { const session = cookies().get('session'); return <ClientNav session={session?.value} />; } ``` `next/headers` працює лише на сервері. Клієнтські компоненти живуть у браузері, де заголовків запиту немає. **Забути `cache: 'no-store'` і отримати застарілі дані** ```tsx // Неправильно - Next.js кешує цей запит за замовчуванням const data = await fetch('/api/live-prices'); // Правильно const data = await fetch('/api/live-prices', { cache: 'no-store' }); ``` App Router кешує `fetch` за замовчуванням. Це заскакує більшість команд, що переходять з Pages Router де fetch за замовчуванням не кешувався. Без `cache: 'no-store'` сторінка повертає дані зі збірки, а не поточного запиту. **Блокувати всю сторінку на повільному API** ```tsx // Неправильно - уся сторінка чекає Stripe перед відправкою HTML export default async function OrdersPage() { const res = await fetch('https://api.stripe.com/v1/orders', { cache: 'no-store' }); // користувач чекає 2+ секунди на перший байт } // Правильно - оболонка вирушає одразу, повільна частина стрімиться import { Suspense } from 'react'; export default function OrdersPage() { return ( <div> <h1>Ваші замовлення</h1> <Suspense fallback={<p>Завантаження...</p>}> <SlowOrders /> </Suspense> </div> ); } ``` Без `Suspense`-межі 2-секундна відповідь Stripe дорівнює 2-секундному TTFB і поганому Lighthouse score. **SSR на всіх маршрутах підряд** Кожен SSR-запит виконує серверний код. При великому трафіку це дорого і погано масштабується. Сторінки з однаковим контентом для всіх - блог, лендінг, документація - мають бути SSG. SSR тримай для сторінок, де контент справді різний для кожного користувача або кожного запиту. ### Де зустрічається в реальних проектах - **Vercel dashboard** - дані білінгу зі Stripe API по сесії кожного юзера. - **Netflix** - персоналізований список перегляду, рендеринг на сервері з preferences кожного запиту. - **GitHub** - сторінки репозиторіїв з актуальним числом комітів. - **IT Lead профіль** - session cookie через `cookies()`, запит у БД за вирішеними задачами, рендер при кожному відкритті. ### Питання на співбесіді **Q:** Чим SSR відрізняється від ISR (incremental static regeneration)? **A:** ISR будує HTML статично, але оновлює його у фоні за розкладом, наприклад `revalidate: 60` означає не частіше разу на хвилину. SSR будує HTML заново на кожен запит. ISR дешевший, SSR завжди актуальний. **Q:** Що відбувається при hydration mismatch? **A:** React виводить попередження в консолі браузера і може перерендерити проблемну частину на клієнті. Зазвичай причина - різні дані на сервері та клієнті. Вирішення: переконайся, що обидві сторони використовують одне джерело даних. **Q:** У чому різниця між React Server Components і SSR? **A:** SSR - це стратегія рендерингу: генерувати HTML на сервері на кожен запит. React Server Components (RSC) - це модель компонентів: вони виконуються лише на сервері, отримують дані inline і не надсилають жодного JavaScript на клієнт. В Next.js App Router вони працюють разом, але це різні концепції. **Q:** Повільний запит до БД додає 1.5 секунди до TTFB. Як вирішити? **A:** Спочатку додай `Suspense`-межу навколо повільного компонента - оболонка вирушить одразу. Перевір наявність індексу на поле запиту і чи вибираєш тільки потрібні поля. Якщо контент не унікальний для кожного юзера - переведи компонент на ISR з коротким `revalidate`. Профілюй через Vercel Speed Insights або `console.time()` щоб точно знайти де витрачається час. ## Приклади ### Базовий: увімкнути SSR через `cache: 'no-store'` ```tsx // app/prices/page.tsx export default async function PricesPage() { const res = await fetch('https://api.example.com/prices', { cache: 'no-store' // Next.js не кешуватиме цю відповідь }); const prices = await res.json(); return ( <ul> {prices.map((item: { id: string; name: string; price: number }) => ( <li key={item.id}>{item.name}: {item.price} грн</li> ))} </ul> ); } ``` Прапор `cache: 'no-store'` - все що потрібно. Next.js бачить динамічний fetch і не включає маршрут у статичну збірку. Свіжі дані на кожен запит. ### Середній рівень: перевірка авторизації через session cookie (App Router) ```tsx // app/profile/page.tsx import { cookies } from 'next/headers'; import { redirect } from 'next/navigation'; import { db } from '@/lib/db'; export default async function ProfilePage() { const cookieStore = cookies(); // автоматично позначає маршрут як динамічний const sessionId = cookieStore.get('session')?.value; if (!sessionId) redirect('/login'); const user = await db.user.findUnique({ where: { sessionId } }); return ( <div> <h1>Вітаємо, {user?.name}</h1> <p>Вирішено задач: {user?.solvedCount}</p> </div> ); } ``` Виклик `cookies()` з `next/headers` сигналізує Next.js що сторінка залежить від запиту. `cache: 'no-store'` тут не потрібен. Сторінка запитує БД при кожному відкритті, авторизація включена. ### Продвинутий: streaming SSR з Suspense для повільного зовнішнього API ```tsx // app/orders/page.tsx import { Suspense } from 'react'; async function OrderList() { // Цей fetch блокує лише OrderList, не всю сторінку const res = await fetch('https://api.stripe.com/v1/charges', { cache: 'no-store' }); const { data: charges } = await res.json(); return ( <ul> {charges.map((c: { id: string; amount: number }) => ( <li key={c.id}>{(c.amount / 100).toFixed(2)} грн</li> ))} </ul> ); } export default function OrdersPage() { return ( <div> <h1>Ваші замовлення</h1> {/* Оболонка вирушає одразу. OrderList стрімиться після готовності. */} <Suspense fallback={<p>Завантаження замовлень...</p>}> <OrderList /> </Suspense> </div> ); } ``` `<h1>` і fallback-текст доходять до браузера ще до того як Stripe відповів. Коли `OrderList` резолвиться - React стрімить готовий HTML і замінює fallback. TTFB залишається низьким навіть з повільним API.Для рев’юераПримітка для модератора (необов’язково)Бачить лише модератор. Прискорює рев’ю.