Чому React Server Components — майбутнє веброзробки
Усім привіт. Мене звати Максим, і я займаюсь веброзробкою вже понад 10 років. Розпочинав працювати ще на PHP, потім Angular перший, а останні пʼять років — React.
Пишу собі на React і щасливий: робота нескладна, тому що вся бізнес-логіка практично завжди робиться на back-end, а я лише вивожу дані та реалізовую взаємодію з користувачем. До чого ж сильно я здивувався, коли побачив концепцію React Server Components. Аби було зрозуміло, про що я, додам скриншот коду з демо на цій презентації:
У статті ми познайомимося з доволі новою концепцією — React Server Components. Проаналізуємо плюси та мінуси цього підходу, дізнаємося, для чого він був створений та побачимо, як на практиці можна його застосовувати. Матеріал розрахований на досвідчених розробників, які вже працюють з React і знайомі з базовою термінологією.
Я вже писав, що займаюсь розробкою понад 10 років, тому бачив і використовував різні архітектури та технології. І після всіх років розвитку технологій ми повернулися на початок — PHP. Хто писав цією мовою, то повинен впізнати код.
Ну й звісно, для тих, хто більше любить відеоформат, ознайомитися з концепцією можна у відео на моєму YouTube каналі.
Що таке серверні компоненти
Якщо узагальнено, то React Server Components — це нова архітектура розробки на React, що дозволяє створювати компоненти, які будуть виконуватися лише на сервері. Це дозволяє нам, наприклад, писати запити безпосередньо в базу даних.
Ви можете поцікавитися, як за допомогою JavaScript бібліотеки на клієнтській стороні, яка буде створена для полегшення розробки UI, можна робити запити в базу даних на сервері? До цього ми дійдемо в практичній частині.
Що реалізовують серверні компоненти:
- Performance — рендеринг компонента або цілих сторінок відбувається на сервері, й клієнту повертається готовий HTML (а якщо бути точним, то не HTML). На цьому кроці треба розрізняти Server Side Rendering (SSR) і React Server Components (RSC).
- Bundle size — зменшується розмір білда, оскільки компоненти та залежності не потрібно відправляти клієнту. Достатньо відправити лише результат його роботи.
- Data fetch — всі дані отримуються на сервері, форматуються, і готовий результат повертається клієнту. Рішення CORS — як бонус.
Покращення продуктивності
Щоб зрозуміти, як покращується продуктивність під час використання React Server Components, потрібно розібратися, як працює серверний рендеринг. У мене є окрема стаття на DOU з порівнянням методів рендерингу в React.
Під час завантаження сторінки, створеної за допомогою React, за замовчуванням ми отримуємо практично порожню сторінку. Далі браузер виконує JS, запускається React, і він уже будує сторінку та за потреби витягує дані з сервера.
Скрипт bundle.js містить усе необхідне для збірки та запуску застосунку, зокрема React, інші сторонні залежності та весь код, який ми написали.
Проблема цього підходу полягає в тому, що на виконання всіх операцій потрібен час. І поки це відбувається, користувач дивиться на порожній білий екран. Ця проблема погіршується з часом: кожна нова функція, яку ми написали, та кожна нова залежність додають ще більше кілобайт до нашого JavaScript пакету, збільшуючи час, за який користувач повинен сидіти та чекати.
Коли застосунок запустився і відрендерив щось користувачу, частіше за все додатково потрібні ще якісь дані з сервера. Ми вибираємо підхід до отримання цих даних, а точніше бібліотеку: Axios, React Query, SWR або Apollo. Клієнт робить запит на back-end, і ми знову очікуємо дані.
Візуалізувати можна це так:
Покращити ситуацію може серверний рендеринг. Навіщо нам рендерити шаблон сторінки на клієнті після завантаження сторінки, якщо це можна зробити на сервері, що є потужнішим і зробить це швидше? Десь на цьому моменті заплакали розробники, які популяризували Single Page Applications (SPA), де основним бенефітом було розвантаження сервера завдяки перенесенню важкої роботи на клієнтів.
Сервер поверне готовий HTML, і користувач уже побачить сторінку. Паралельно браузер завантажує JS і виконує цікаву процедуру, яка називається hydration.
Після завантаження JavaScript-пакету React швидко пройде весь наш застосунок, будуючи віртуальний макет інтерфейсу та «вписуючи» його в реальний DOM, додаючи обробники подій, запускаючи будь-які ефекти й так далі. Тобто додаємо живої води в сухий HTML-шаблон.
Покращений підхід виглядає так:
Ситуація вже краща, сторінка завантажується швидше, але ж користувачі приходять на сайт не заради порожнього шаблону — потрібні дані. То чому б отримання даних не перенести також на сервер?
Оскільки сервер потужніший, дані лежать «ближче», та й кеш налаштований повинен бути — відповідь набагато швидша, користувач бачить готову сторінку. Разом із цим, у нас покращуються три вебметрики:
- First Paint. Користувач більше не дивиться на порожній білий екран. Загальна структура сторінки вже відобразилася, але контент ще відсутній. Іноді це називається FCP (Перший Візуальний Зміст).
- Content Paint. Тепер сторінка містить те, що цікавить користувача. Ми витягли дані з бази даних і відобразили їх у інтерфейсі. Іноді це називається LCP (Найбільший Змістовий Візуальний Зміст).
- Page Interactive. React завантажений, і наш застосунок відображений або активований. Інтерактивні елементи тепер повністю реагують на дії користувача. Іноді це називається TTI (Час До Інтерактивності).
Ми уникнули «стрибків туди-сюди», коли сервер повертає JS, браузер рендерить застосунок і цей застосунок знову робить запит на бекенд, щоб отримати необхідні дані. Але як ми цього досягли? React із коробки цього не дозволяє. Потрібно використовувати додаткові фреймворки, типу Next.js, Gatsby, Remix або їм подібні. Додатково змінюється архітектура застосунку, оскільки ми прив’язуємось до реалізації фреймворку.
Недоліки підходу:
- Ця стратегія працює лише на рівні адреси для компонентів, розташованих у самому верхньому рівні дерева компонентів, тобто сторінки. Ми не можемо використовувати цей підхід в будь-якому компоненті.
- Кожний метафреймворк розробив свій власний підхід. У Next.js є свій спосіб, у Gatsby — інший, у Remix — ще один. Немає стандартизації.
- Усі наші компоненти React завжди будуть гідратуватися (hydrate) на клієнтському боці, навіть коли цього не потрібно.
Команда розробників React роками працювала над розв’язання цього питання і прийшла до єдиного, офіційного, стандартизованого підходу під назвою React Server Components.
Зменшуємо розмір білда — bundle size
Робимо це після всіх оптимізацій із серверним рендерингом. Єдине що не змінилось — це етап Download JavaScript. Нам усе ще потрібно завантажити увесь JS, що потрібний і не потрібний на сторінці, аби користувач міг розпочати взаємодію із сайтом. Якщо нам не потрібен якийсь код або складна логіка компонента, а лише результат його роботи, чи маємо ми завантажувати цей код? У цьому випадку розбиття бандла на чанки за допомогою того ж самого lazy()
не спрацює.
Щоб бути більш наочним і зрозуміти проблему, розгляньмо наступний приклад. Вам на сторінці потрібно вивести форматовану дату та час. У JavaScript з коробки красивого рішення нема, а Intl.DateTimeFormat може не відповідати нашим вимогам. У цьому разі ми, як ліниві професійні розробники, шукаємо бібліотечку, яка це зробить. Знаходимо ідеальне рішення — Moment.js. Бібліотека актуальна, підтримується, на npmjs бачимо, що за тиждень завантажується 20.5 мільйонів разів — бінго! Додаємо в проєкт.
От усе було б ідеально, якби не її розмір. Якщо зайти на bundlephobia, можна здивуватися. Поточна версія у зжатому вигляді займає 72,1 КБ. І не факт, що tree-shaking у вашому випадку дозволить зменшити цей розмір.
Зазвичай у застосунках використовується не одна бібліотека, а десятки (якщо не сотні), і кожну з них потрібно зібрати до купи та надіслати клієнту, навіть якщо нам потрібен лише результат їхньої роботи, яким може бути одне слово чи кілька символів.
Коли однією з переваг використання React Server Components згадується зменшення Bundle size — це не про оптимізацію ваших функціональних компонентів, це про всі ці монструозні бібліотеки, які ми тягнемо в код і використовуємо у функціональних компонентах.
Отримання даних із сервера — data fetching
Щоб щось відобразити на сторінці, це щось потрібно отримати з сервера. Не завжди для запитів на бек-енд розробники використовують вбудовану функцію fetch()
. Завдання зазвичай набагато складніші, тому й рішення потрібні складніші. У проєкт додаються різні Axios, SWR, React Query, Apollo або інші. Це означає додаткові кілобайти в бандл плюс свій підхід до роботи із запитами. На стороні бек-енд теж необхідні зміни, адже всі ці бібліотеки працюють з RESTfull, і їм потрібний endpoint, куди робити запит.
Якщо ми використовуємо серверні компоненти, можна робити запити в базу даних напряму! Звучить як космос, але таке вже можливо. Ось приклад такого компонента:
Водночас ніхто не забороняє в серверному компоненті використовувати будь-яку з раніше згадуваних бібліотек для отримування даних, наприклад з мікросервісу тим самим REST. Код цих бібліотек не додається в клієнтський бандл.
Оскільки код серверного компонента не повертається в браузер, то можна не хвилюватися про логіни, пароль до бази даних, вони клієнту не відправляться — лише результат.
Ну і як бонус — CORS. Обмеження на запити між різними доменами існує лише в браузері. Одним з рішень завжди було використання бек-енд як проксі. RSC вирішують це обмеження з коробки, адже вони і є цим беком.
Особливості серверних компонентів
Той, хто знайомий з React, може здивуватися синтаксису компонента зображеного вище. Функціональні компоненти не можуть бути асинхронними. Вони також не можуть опрацьовувати сайд-ефекти таким способом. А отримання даних з бази даних і є цим сайд-ефектом.
Ключова особливість RSC — вони не ререндеряться! UI згенерований на сервері один раз і відправлений клієнту, більше жодних мутацій.
Оскільки ререндерів нема, величезна частина React API недоступна на сервері. Стейт не змінюється, отже useState
не можна використовувати. Ефекти також не можна використовувати, адже вони запускаються після рендера — з useEffect
також прощаємось на сервері. Для багатьох це буде полегшенням, адже тепер можна працювати більше в NodeJS стилі, без хуків.
Серверні компоненти не мають доступу до APIs браузерів, наприклад LocalStorage.
Підсумуємо обмеження:
- неможливо використовувати стейт і ефекти, ніяких
useState
,useEffect
; - відсутність доступу до APIs браузера;
- кастомні хуки, що використовують стейт або ефекти, також недоступні.
Коли використовувати серверні компоненти та нова термінологія
Використовуйте завжди та всюди, де це можливо. Не даремно ж Next.js зробив усі компоненти серверними за замовчуванням. Коли ж потрібно використовувати стейт, ефекти або APIs браузера — тоді уже клієнтські.
Ми вже знаємо про серверні компоненти, які запускаються на сервері. Як тоді називаються компоненти, що запускаються на клієнті? Ніколи не повірите — клієнтські компоненти (Client Components). Неймінг залишає бажати кращого.
Може здатися, що все просто та зрозуміло — серверні на сервері, а клієнтські в браузері на клієнті. Але ніт. Клієнтські також виконуються на сервері.
Рендериться на сервері |
Рендериться на клієнті | |
Server Component |
Так |
Ні |
Client Component |
Так |
Так |
Практика
Концепція серверних компонентів звучить дуже складно. Зрозуміти її не так просто з першого разу, особливо, якщо ми все ще думаємо, що вона реалізується просто останньою версією React. Для повноцінного використання цієї архітектури нам потрібен Next.js, оскільки, згідно з офіційною документацією, саме цей фреймворк реалізовує серверні компоненти. Так, це виглядає дивно, що core-functionality React не реалізована в самому React безпосередньо.
Уточнюю: React Server Components усе ще не production-ready, тому їх використання є скоріше в навчальних цілях, щоб спільнота оцінила та протестувала. Це ж саме було свого часу зроблено й з хуками.
Хоч ця концепція і складна для розуміння, її реалізація, а точніше використання, є достатньо простою. Створімо перший серверний компонент.
- Створити застосунок за допомогою Next.js —
npx create-next-app@latest
- Йдемо за пунктами, що пропонує генератор, і головне, що нам потрібно вибрати, — це пункт
Would you like to use App Router? (recommended)
. Вказуємоyes
.
В принципі й все. Тепер у нас є програма, згенерована за допомогою Next.js, де всі компоненти, за замовчуванням, є серверними.
Звісно, для повноцінної роботи з серверними компонентами треба ще вивчити фреймворк Next.js, а з ним і TypeScript бажано, але це вже можна вважати приємним бонусом.
Відкриємо файл src/app/page.tsx
, викинемо з нього весь контент за замовчуванням і додамо лише Hello world. На цьому моменті я б рекомендував переглянути моє відео на YouTube про Серверні компоненти, там робота з кодом буде наочніша.
Цей компонент уже є серверним, і він не буде доданим в бандл. Тоді виникає питання: а як він додасться на сторінку? Якщо відкрити код сторінки в браузері, після запуску застосунку на 3000 порті, ми побачимо приблизно такий код (частину коду для наочності я видалив):
Окрім звичайного серверного рендерингу, в коді зʼявляється дещо новеньке — скрипти за викликом self.__next_f.push
. Цього коду немає під час звичайного серверного рендерингу. Це і є тим результатом роботи React-компоненту, який нам потрібен. Усю решту React робить під капотом. Ми не повинні задумуватись над цим синтаксисом.
А як тепер створити Клієнтські компоненти? Для цього потрібно використати директиву `use client`
, яка чимось схожа на уже нам знайому «use strict». Тобто створюємо звичайний компонент, але з додатковим рядком зверху:
Без директиви ми отримаємо таку помилку:
Оскільки пам’ятаємо, що всі компоненти за замовчуванням серверні та в них не можна використовувати стейт.
Додамо до проєкту moment.js і зіставимо розмір білда. Для порівняння створимо два ідентичних проєкти на Next.js, але один на основі App Router, тобто серверних компонентів, інший на основі Pages Router, тобто стандартні компоненти з SRR в обох випадках. Розмір фінальних JS-файлів візьмемо з поля First Load JS, що показує Next.js виконавши команду yarn build.
App Router, kb |
Pages Router, kb | |
Порожній проєкт |
80.5 |
79.6 |
З доданим Moment.js |
80.5 |
99 |
Результат доволі наочний, хоч і додано було лише одну бібліотеку. На щастя, tree shaking зменшив фінальний розмір moment.js, але різниця все ще суттєва.
Висновки
Пункти, що не розглянуті в цій статті, але варті уваги:
- Взаємодія клієнтських та серверних компонентів — згідно з документацією, можна в серверних використовувати клієнтські та навпаки. Тоді виникає питання: якщо серверні не ререндеряться, то що робити, коли вони є елементами клієнтських компонентів, де є стейт, і під час зміни якого викликається рендер?
- Як дебажити серверні компоненти?
console.log()
у цьому випадку не допоможе, та й місце зупинки дебагера ніде поставити, адже компонента нема на клієнті, лише результат роботи. - Suspense та Streaming SSR архітектура — ідеальне доповнення до React Server Components.
Й до підсумків:
- React Server Components (RSC) — це нова архітектура та підхід до розробки вебзастосунків за допомогою React.
- Для використання всіх передових можливостей бібліотеки React потрібно вивчити та використовувати фреймворк Next.js;
- Зʼявляється кардинально нова можливість — писати код для сервера. Отже, замовники зможуть перекласти частину роботи з back-end розробників front-end, тобто реалізацію частини бізнес-логіки. Чи будуть нам більше за це платити? Сумніваюсь. Але роботи буде більше однозначно.
React Server Components разом з Next.js дозволяють реалізувати інноваційні та потужні рішення для веброзробки. Хоча ця технологія ще не є готовою для використання в продуктивних проєктах, вона вже зараз відкриває нові можливості та розширює горизонти для майбутнього веброзробки.
Усі статті, обговорення, новини про Front-end — в одному місці. Підписуйтеся на телеграм-канал!
Найкращі коментарі пропустити