Запропонувати правкуПокращити цю статтюДопрацюйте відповідь до «Що таке прогресивний рендеринг у веб-розробці». Ваші зміни проходять модерацію перед публікацією.Потрібне підтвердженняКонтентЩо ви змінюєте🇺🇸EN🇺🇦UAПереглядЗаголовок (UA)Коротка відповідь (UA)**Прогресивний рендеринг** (progressive rendering), це відображення контенту сторінки по мірі готовності, без очікування на повне завантаження документа. ```html <img fetchpriority="high" src="hero.jpg" /> <!-- вище згину: одразу --> <img loading="lazy" src="product.jpg" /> <!-- нижче згину: відкладено --> ``` **Головне:** комбінація lazy loading, SSR streaming і Suspense скорочує FCP на 50-70% на повільних мережах.Показується над повною відповіддю для швидкого нагадування.Відповідь (UA)Зображення**Прогресивний рендеринг** (progressive rendering), це підхід, при якому контент сторінки з'являється по мірі готовності, а не після завантаження всього документа. ## Теорія ### TL;DR - Аналогія: газета, яка друкується посторінково. Заголовок читаєш одразу, поки 10-та сторінка ще в роботі - Головна ідея: спочатку малюємо контент вище згину, решту відкладаємо - Скорочує Time to Interactive на 50-70% на повільних мережах - Використовуй, якщо FCP перевищує 1с або мобільні користувачі йдуть зі сторінки. Пропускай для сторінок менше 100KB - Основні інструменти: `loading="lazy"`, `IntersectionObserver`, React `Suspense`, SSR streaming ### Швидкий приклад ```html <!-- Вище згину: завантажуємо одразу --> <img src="hero.jpg" fetchpriority="high" width="1200" height="600" /> <!-- Нижче згину: відкладаємо до прокрутки --> <img loading="lazy" src="product.jpg" width="300" height="300" alt="Продукт" /> ``` ```jsx // React: показуємо skeleton, рендеримо компонент коли дані прийшли <Suspense fallback={<Skeleton />}> <HeavyDashboard /> </Suspense> ``` Браузер малює hero-зображення відразу. Зображення продукту підтягується лише тоді, коли користувач прокручує до нього. React-компонент показує skeleton до того, як дані будуть готові. Три різні інструменти, один результат. ### Головна відмінність від звичайного завантаження При стандартному завантаженні браузер чекає на повну HTML-відповідь, перш ніж намалювати щось корисне. При прогресивному рендерингу він починає малювати DOM-вузли одразу, як тільки HTML-парсер отримує перші чанки. Preloader thread паралельно сканує потік наперед у пошуку тегів `<script>`, `<link>` і `<img>`, щоб запустити завантаження ресурсів раніше (HTML spec, §12.2.6). Результат: користувач бачить реальний контент за 200ms замість порожнього екрана протягом 2-3 секунд. ### Чотири основні техніки **Ліниве завантаження (lazy loading)** відкладає зображення, відео і компоненти до моменту, коли вони потрапляють у viewport. Нативний атрибут `loading="lazy"` покриває більшість кейсів у Chrome 76+. Для компонентів у React: `React.lazy()` з `Suspense`. **SSR streaming** надсилає HTML з сервера частинами. React 18 через `renderToPipeableStream` і Next.js App Router підтримують цей підхід. Браузер рендерить перший чанк, поки сервер ще генерує решту. Типовий приклад: шапка дашборду з профілем приходить за 200ms, стрічка постів за 800ms. **Skeleton screens і плейсхолдери** замінюють порожнє місце приблизними контурами майбутнього контенту. Це стабілізує макет і дає користувачу сигнал, що завантаження відбувається. Без стрибків контенту, без порожнього екрана. **Прогресивна гідратація (progressive hydration)** підключає JavaScript-поведінку тільки до тих частин сторінки, які потрібні користувачу першими. `<Script strategy="lazyOnload" />` у Next.js запускає сторонні скрипти після того, як сторінка вже стала інтерактивною. ### Як це працює всередині браузера HTML-парсер обробляє байти по мірі надходження і будує DOM поступово. Вузли передаються рендереру, як тільки вони в повному стані. Preloader thread паралельно сканує вперед у пошуку `<script>`, `<link>` і `<img>`, щоб запустити завантаження ресурсів раніше. У React 18 межі `Suspense` дозволяють серверу пропустити піддерево, яке ще не готове, надіслати решту HTML, а потім стрімнути пропущений шматок окремим чанком, коли його дані вирішаться. Клієнт гідратує кожен чанк незалежно. Саме тому шапка може бути вже робочою, поки стрічка ще завантажується. ### Коли використовувати - E-commerce сітки з 50+ зображеннями: `loading="lazy"` на все, що нижче згину - Дашборди з незалежними джерелами даних: `Suspense` навколо кожного віджета, щоб швидкі дані з'являлися одразу - Контентні сайти на Next.js: streaming через App Router скорочує FCP з 3с до менше 1с - SPA-додатки з 50+ компонентами: `React.lazy` + code splitting тримають початковий бандл малим - Пропускай для статичних сторінок менше 100KB і сторінок, де SEO-краулеру потрібен повний HTML без SSR ### Типові помилки **1. `loading="lazy"` на hero-зображення.** ```html <!-- Неправильно: затримує LCP на 2+ секунди --> <img loading="lazy" src="hero.jpg" /> <!-- Правильно: кажемо браузеру, що це зображення важливе --> <img fetchpriority="high" src="hero.jpg" /> ``` `loading="lazy"` прибирає зображення зі сканера попереднього завантаження. Для hero це рівно протилежне тому, що потрібно. **2. `React.lazy` без межі `Suspense`.** ```jsx // Неправильно: unhandled promise rejection, білий екран на 3с const Chart = React.lazy(() => import('./Chart')); <Chart /> // Правильно <Suspense fallback={<Spinner />}> <Chart /> </Suspense> ``` Без межі відхилений Promise спливає вгору і ламає дерево рендерингу. **3. SSR streaming без обробки помилок.** ```tsx // Неправильно: запит падає посеред стріму, Node некоректно обриває з'єднання async function UserData() { const data = await fetch('/api/user').then(r => r.json()); return <div>{data.name}</div>; } // Правильно: огорни в error.js (Next.js) або ReactErrorBoundary ``` До моменту помилки частина HTML вже надіслана клієнту. Без error boundary клієнт отримує зламану розмітку і гідратація падає. **4. CLS від контенту, що з'являється пізно.** Зображення і компоненти, що завантажуються після першого рендера, зсувають існуючий контент вниз. Резервуй місце заздалегідь: ```css img { width: 100%; aspect-ratio: 16 / 9; /* тримає місце до завантаження */ } ``` Або встановлюй явні `width` і `height` на тегах `<img>`. Обидва підходи дають браузеру інформацію про розмір до того, як зображення прийде, і тримають CLS нижче 0.1. **5. Несумісність гідратації з lazy-зображеннями в SSR.** ```jsx // Неправильно: сервер рендерить без атрибута, клієнт додає loading="lazy" function Post({ post }) { return <img loading="lazy" src={post.image} />; } // React 18 виводить попередження і перерендерює повністю, // губляться переваги streaming // Правильно: next/image генерує однаковий вивід на сервері і клієнті import Image from 'next/image'; <Image src={post.image} priority={false} /> ``` ### Де застосовується - Next.js 14 App Router: `Suspense` + React Server Components стрімлять RSC-пейлоади. Дашборд Vercel побудований на цьому підході - Remix v2: `defer()` стрімить повільні дані без блокування швидких на тому самому роуті - SvelteKit 2: функції `load` стрімлять чанки, популярно на контентних сайтах - Shopify Hydrogen: побудований на streaming-моделі Remix для сторінок продуктів - YouTube: `IntersectionObserver` + `loading="lazy"` на сітках мініатюр - Express / Node.js: `res.write()` стрімить HTML до того, як всі дані готові ### Питання на співбесіді **Q:** Яка різниця між прогресивним рендерингом і code splitting? **A:** Code splitting ділить JS на окремі бандли, якими керує webpack або Vite. Прогресивний рендеринг контролює, коли контент з'являється у DOM. Разом вони працюють через `React.lazy()`: він ділить бандл, `Suspense` прогресивно рендерить результат. **Q:** Як прогресивний рендеринг впливає на Core Web Vitals? **A:** При правильному підході скорочує FCP і LCP, стрімлячи критичний HTML раніше. Головний ризик: CLS. Якщо вставляти контент без зарезервованого місця, макет стрибатиме і цей показник погіршиться. **Q:** Як реалізувати lazy loading зображень без нативного `loading="lazy"`? **A:** Через `IntersectionObserver`. Спостерігаєш за елементом. Коли він входить у viewport, переносиш `data-src` у `src`. Працює в Chrome 58+, Firefox 55+ і будь-якому середовищі з підтримкою Observer API. **Q:** Як React 18 streaming поводиться з межею `Suspense`, яка так і не вирішується? **A:** Сервер тримає стрім відкритим до налаштованого тайм-ауту, потім надсилає те, що готово. У Next.js це налаштовується через `loading.js`. Невирішені межі надсилають HTML fallback, тому клієнт ніколи не зависне на порожній відповіді. **Q:** Як SSR streaming у React Server Components взаємодіє з графом модулів під час збірки? **A:** RSC-межі виступають точками розбиття стріму. Next.js 14 з Turbopack заздалегідь завантажує RSC-чанки у фоні, щоб сусідні модулі не блокували один одного. За внутрішніми бенчмарками Next.js це дало приблизно 35% приріст продуктивності порівняно з webpack. ## Приклади ### Базовий: ліниве завантаження сітки зображень ```html <!DOCTYPE html> <html> <head> <title>Сітка продуктів</title> </head> <body> <!-- Hero: малюємо одразу, резервуємо місце --> <img src="hero.jpg" fetchpriority="high" width="1200" height="600" alt="Розпродаж" /> <!-- Продукти: завантажуємо по мірі прокрутки --> <div class="grid"> <img loading="lazy" src="prod1.jpg" width="300" height="300" alt="Продукт 1" /> <img loading="lazy" src="prod2.jpg" width="300" height="300" alt="Продукт 2" /> <img loading="lazy" src="prod3.jpg" width="300" height="300" alt="Продукт 3" /> </div> </body> </html> ``` Hero має `fetchpriority="high"`, тому браузер завантажує його першим. Зображення продуктів мають явні `width` і `height`, тому браузер резервує для них місце ще до завантаження. Без CLS, без затримки hero. Це мінімальна конфігурація для будь-якої сторінки з великою кількістю зображень. ### Середній рівень: SSR streaming у Next.js App Router ```tsx // app/dashboard/page.tsx import { Suspense } from 'react'; async function Profile() { // Швидкий запит: ~100ms const user = await fetch('https://api.example.com/user').then(r => r.json()); return <div>{user.name}'s Dashboard</div>; } async function Posts() { // Повільний запит: ~900ms const posts = await fetch('https://api.example.com/posts').then(r => r.json()); return <ul>{posts.map(p => <li key={p.id}>{p.title}</li>)}</ul>; } export default function Dashboard() { return ( <Suspense fallback={<div>Завантаження профілю...</div>}> <Profile /> <Suspense fallback={<div>Завантаження постів...</div>}> <Posts /> </Suspense> </Suspense> ); } // Результат: HTML профілю стрімиться за ~100ms, пости за ~900ms. // FCP: 0.8с проти 3.2с при блокуючому SSR. ``` Дві вкладені межі `Suspense` дозволяють кожному джерелу даних стрімитися незалежно. Профіль не чекає на API постів. Якщо запит постів падає, лише внутрішня межа показує помилку. Профіль залишається цілим. Цей патерн скорочує FCP дашборду вдвічі і більше. ### Просунутий рівень: несумісність гідратації при SSR streaming ```jsx // Патерн, що викликає попередження гідратації React 18 у продакшені // Неправильно: сервер рендерить зображення без атрибута loading. // Клієнтський компонент додає loading="lazy" після монтування. // React бачить розбіжність і перерендерює весь піддерево. function PostCard({ post }) { return ( <Suspense fallback={<div>Завантаження...</div>}> {/* Сервер: немає loading. Клієнт: loading="lazy". Мismatch. */} <img loading="lazy" src={post.coverImage} alt={post.title} /> </Suspense> ); } // Console: Warning: Prop `loading` did not match. // React викидає стрімлений HTML і перерендерює з нуля. // Правильно: next/image генерує однаковий вивід на сервері і клієнті import Image from 'next/image'; function PostCard({ post }) { return ( <Image src={post.coverImage} alt={post.title} width={800} height={400} priority={false} // однаково на сервері і клієнті /> ); } ``` Несумісність виникає тому, що гідратація React порівнює серверний HTML з тим, що клієнт згенерував би самостійно. При розбіжності React викидає серверний HTML і перерендерює з нуля. Весь виграш від streaming для цього піддерева втрачається. `next/image` з `priority={false}` генерує однаковий вивід з обох боків, тому гідратація проходить без помилок.Для рев’юераПримітка для модератора (необов’язково)Бачить лише модератор. Прискорює рев’ю.