Браузер, сервер чи час збірки: карта прийняття рішень для архітектур фронтенду
Подивіться, як SSG, SSR, стрімінг, серверні компоненти, BFFs, обробка на краю мережі, модульні моноліти та мікро-фронтенди кожен відповідає на одне запитання: де має відбуватися обробка?
Обговорення архітектури, особливо інтерв’ю з керівними фронтенд-розробниками, рідко цікавляться тим, що ви створили. Їх цікавить причина, чому ви це зробили саме так, і відповідь „це те, що вже було у команди“ не є прийнятною. Хороша новина полягає у тому, що майже кожен шаблон архітектури фронтенду відповідає на одне й те саме основне питання: скільки роботи має виконуватися у браузері, скільки на сервері та скільки під час збірки? Як тільки ви побачите SSR, Server Components, backends-for-frontends та micro-frontends як різні способи визначення цієї межі, вибір між ними та обґрунтування цього вибору стає набагато простішим.
Кілька слів про джерела: описані нижче ситуації у компаніях походять з публічних технічних матеріалів та офіційної документації, і вони також посилані як такі.
Як змінилася межа між клієнтом та сервером
Рання веб-технологія була простою. Статичні HTML-файли завантажувалися майже миттєво, і в них було дуже мало елементів, які могли зламатися.
Потім фреймворки MVC з боку сервера, такі як Django, Rails та ASP.NET, почали генерувати HTML при кожному запиті. Тепер сторінки могли відображати динамічні дані, але кожен клік означав повне перезавантаження сторінки.
Багатосторінкові додатки, створені за допомогою React, Vue чи Angular, сильно змінили ситуацію в протилежному напрямку. Браузер взяв на себе функції маршрутизації, керування станом, перевірки даних та іноді навіть автентифікації. В результаті з’явився інтерфейс, який здається миттєвим у використанні, але лише після завантаження. Однак є один недолік: великі пакети JavaScript, повільне перше відображення та гірша знаходженість контенту. Google дійсно виконує JavaScript, проте обробка може затримувати індексацію великих сайтів, а багато інших пошукових роботів, включаючи ботів для перегляду контенту в соціальних мережах та багато штучних інтелектуальних роботів, взагалі не виконують скрипти. Контент, який вони не можуть ефективно побачити, для них просто не існує.
Якщо дивитися здалеку, історія архітектури фронтенду — це серія змін між тонкими клієнтами, де більшу частину роботи виконує сервер, та товстими клієнтами, де цю роботу виконує браузер. Кожен підхід, описаний у решті цього посібника, є іншою позицією цього розділення.
Backend-for-Frontend: переформатування одного API на кожного клієнта
У конфігурації Backend-for-Frontend (BFF) команда фронтенду запускає власний тонкий сервіс перед справжніми бекенд-сервісами. Цей сервіс виконує одну функцію: перетворює дані бекенду саме у те, що потрібно конкретному клієнту.
Цю модель зазвичай пов’язують із SoundCloud. Близько 2013 року, під час перетворення моноліту на мікросервіси, компанія виявила, що клієнти для вебу, iOS та Android конкурують за один спільний API, який не підходив жодному з них. Кожен клієнт отримав власний легкий бекенд. Філ Кальсадо, який працював над цією міграцією, пізніше детально описав її історію, а стаття Сема Ньюмана перетворила її на широко цитований опис моделі, який і дав їй назву. Netflix самостійно дійшла до схожого рішення за допомогою адаптерних шарів, спеціалізованих під кожен пристрій, оскільки додаток для телевізора та додаток для телефону потребують зовсім різних даних.
Розгляньмо конкретний випадок. Мобільний додаток потребує назви продукту, ціни та мініатюри. Веб-додаток, крім того, потребує відгуків, рівня запасів та схожих товарів. Один спільний кінцевий пункт або надсилає мобільному додатку занадто багато інформації, або веб-додатку — занадто мало, тож командам доводиться додавати параметри запиту для компенсації.
BFF вирішує цю проблему, надаючи кожному типу клієнта індивідуальну налаштовану відповідь. Наведена нижче служба Express, яка використовує вбудовану функцію fetch у Node 18 та новіших версіях, викликає API продукту та повертає лише чотири поля, перейменовуючи title на name та скорочуючи список зображень до однієї мініатюри:
// bff.js — Node 18+, fetch is built in
import express from 'express';
const app = express();
const API = 'https://api.example.com';
app.get('/products/:id', async (req, res) => {
const response = await fetch(`${API}/products/${req.params.id}`);
const data = await response.json();
res.json({
id: data.id,
name: data.title,
price: data.price,
thumbnail: data.images[0],
});
});
app.listen(4000, () => console.log('BFF listening on :4000'));
Клієнт запитує /products/123 та отримує саме ту структуру, яку хоче, без зайвих полів та додаткових запитів-відповідей. У продакшн-коді також варто перевіряти response.ok перед обробкою даних, а також захищатися від продуктів без зображень, оскільки data.images[0] передбачає наявність принаймні одного зображення.
Цей фактор має отримати більше уваги, ніж зазвичай. BFF — це ще один сервіс, який потрібно розгортати, контролювати та підтримувати у працездатному стані. Якщо він зламається, зламається й інтерфейс користувача, навіть якщо справжній бекенд працює ідеально. Це не безкоштовна інфраструктура; це додаткова точка відмови, головною перевагою якої є зручність для команди фронтенду. Щоб дізнатися більше про створення такого сервісу всередині додатку Next.js, перегляньте статтю про перетворення обробників маршрутів Next.js на спеціалізований шар BFF.
Стратегії рендерингу: де створюється HTML
За останні роки процес рендерингу змінився більше, ніж будь-яка інша сфера, і на практиці категорії змішуються ще більше, ніж свідчать діаграми.
Статичне генерування та поступове оновлення
Статичне генерування сайтів (SSG) рендерує кожну сторінку під час створення та надає прості файли через CDN. Немає нічого швидшого чи дешевшого для розповсюдження, але контент залишається незмінним до наступного розгортання.
Поступове статичне оновлення (ISR) дозволяє автоматично оновлювати контент: сторінка може перегенеруватися на фоні через встановлений інтервал. Таким чином зберігається швидкість роботи статичних файлів без необхідності щоразу перерозгортати сайт при зміні контенту.
Рендеринг з боку сервера та стрімінг
Server-Side Rendering (SSR) генерує HTML для кожного запиту. У своїй класичній формі сервер надсилає повну сторінку, а потім браузер її активує: він завантажує пакет JavaScript та додає обробники подій та стан до маркапу, який вже відображається на екрані.
У React 18 було введено стріминговий SSR через renderToPipeableStream, який надсилає частини HTML як тільки кожна частина структури готова, замість того щоб чекати на найповільніший компонент. Стріминг є звичайним вибором для нових додатків, хоча багато продакшн-додатків досі без проблем використовують класичний SSR усе одночасно.
Server Components, острови та можливість відновлення
React Server Components (RSC) та архітектури типу „острови“ йдуть ще далі: лише інтерактивні частини сторінки передають JavaScript. Статичний контент залишається звичайним HTML без необхідності його обробки. „Острови“ Astro застосовують цю ідею поза межами React. Qwik йде ще далі завдяки функції відновлення роботи, яка значною мірою уникає обробки контенту шляхом серіалізації стану додатку в HTML, щоб клієнт міг продовжити роботу з того моменту, де зупинився сервер.
У наведеному нижче прикладі показана сторінка Server Component у Next.js App Router. Починаючи з версії Next.js 15, params є Promise, який необхідно очекувати, тому компонент розбирає id лише після виконання await params. Завантаження даних відбувається на сервері, тож назва товару та ціна надходять у вигляді вже обробленого HTML:
// app/products/[id]/page.tsx (Next.js 15+)
import { fetchProduct } from '@/lib/api';
export default async function ProductPage({
params,
}: {
params: Promise<{ id: string }>;
}) {
const { id } = await params;
const product = await fetchProduct(id); // runs on the server
return (
<main>
<h1>{product.name}</h1>
<p>${product.price}</p>
<AddToCartButton productId={product.id} />
</main>
);
}
Лише AddToCartButton містить JavaScript з боку клієнта. Щоб це було можливим, він має знаходитися у власному файлі, позначеному директивою 'use client', та імпортуватися у сторінку; для стислості у фрагменті коду пропущено цей імпорт. У результаті утворюється невеликий пакет, який є інтерактивним там, де це необхідно, а в усьому іншому залишається статичним.
Обробка на Edge та причини часткового зміни підходу індустрією
Рендеринг на краю мережі є одним із найяскравіших прикладів у сучасному фронтенді ідеї, яка тестується публічно та постійно вдосконалюється.
Приблизно з 2021 по 2023 рік аргументи на користь цього підходу були переконливими: необхідно використовувати SSR на краю мережі, на таких платформах, як Cloudflare Workers чи Vercel Edge Functions, у дата-центрі, розташованому поблизу кожного користувача. Коротша відстань означала швидшу загрузку сторінок. Vercel активно просував цей підхід.
Пізніше Vercel відкрито змінив свою позицію; тодішній віце-президент з продукту описав ситуацію як "це мене обдурило". Причина є показовою. Обробка даних має знаходитися поблизу користувача, але водночас — поблизу даних, причому більшість баз даних розташовані в одному регіоні. Функція на краю мережі в Токіо, яка здійснює кілька поїздок до бази даних у Вірджинії, часто працює повільніше, ніж просте відтворення контенту у Вірджинії. Коли Vercel перевірив це на власному продукті v0, з’ясувалося, що звичайне відтворення за допомогою Node.js працює ефективніше, ніж відтворення через функції на краю мережі. Пізніше Vercel відмовився від окремих функцій на краю мережі та тепер рекомендує використовувати середовище Node.js із обробкою даних у тому ж регіоні, що й база даних; для отримання точної інформації про статус кожного середовища перегляньте актуальну документацію до платформи.
Те, що залишилося, — це більш вузьке розуміння: негайно доставляти статичну оболонку сторінки з краю мережі, а потім передавати динамічні частини з обчислювальних ресурсів, розташованих поруч із даними. Саме це робить Partial Prerendering; у поясненні щодо часткового попереднього оброблення та одночасного відображення описані механізми цього процесу.
Cloudflare Workers залишається справжньою платформою SSR на краю мережі та добре функціонує, коли самі дані розподілені по всьому світу. Важливий урок полягає у тому, що локальність даних зазвичай краща за локальність користувача. Розуміння причин, чому індустрія змінила свою думку, цінніше, ніж просте знання модного терміна.
Модульний моноліт фронтенду
Коли односторінковий додаток розростається за межі кількох команд, плоский репозиторій стає ризикованим. Усі редагують однакові спільні компоненти, і ніхто не знає, хто є власником чого.
Модульний моноліт розділяє кодову базу на два шари, залишаючи один з них придатним для розгортання:
- Шар платформи, яким керує команда платформи, що містить систему дизайну, спільні інструменти, облік подій та схожу інфраструктуру.
- Шар домену, що складається з папок функцій, таких як
user/абоpayments/, кожна з яких належить відповідній команді функцій.
Ця мотивація схожа на чисту або гексагональну архітектуру, без більшої частини формалізму. Повна чиста архітектура зазвичай є надмірною для фронтенду; кнопці та запити на отримання даних не потребують трьох рівнів абстракції між собою. Важливими є чітке визначення власності та суворе дотримання меж, наприклад, правила лінтингу, які не дозволяють одній області імпортувати внутрішні елементи іншої.
Мікро-фронтенди: незалежність за певну ціну
Архітектура мікро-фронтенду розглядає кожну область як окрему міні-додаток, який можна розгортати окремо; зазвичай він завантажується під час виконання шель-додатком за допомогою механізму на кшталт Webpack Module Federation.
Ви отримуєте справжню автономію: команди можуть випускати продукти за власним графіком та, у разі справжньої необхідності, навіть використовувати різні фреймворки. Zalando, IKEA та DAZN описували впровадження цього підходу у масштабах, завжди з великими інженерними командами та значними інвестиціями у спільні інструменти. micro-frontends.org залишається стандартним джерелом інформації щодо повних прикладів використання.
Способи збою, які справді завдають шкоди
Проблема, яка постійно зустрічається у звітах про реальні інциденти, — це не змішування фреймворків. Це зміна спільних залежностей. Оден віддалений додаток оновлює спільну бібліотеку, тоді як інший — ні, і раптово на одній сторінці починають працювати дві копії React, які конкурують за один і той самий DOM. Module Federation може визначати спільні синглтони та діапазони версій, щоб запобігти цьому, але лише за умови, що команди погоджуються та дотримуються цих обмежень. Саме ця проблема координації завдає командам найбільших проблем, значно більше, ніж зазвичай згадувана „складність“.
Існує також попереджувальний приклад у протилежному напрямку. За повідомленнями, Spotify кілька років тому експериментувала з підходом мікро-фронтенду на основі iframe у своєму десктопному клієнті, а згодом об’єднала все в єдину архітектуру, частково тому, що з’єднання окремих компонентів коштувало дорожче, ніж користь від їхньої незалежності. Навіть у великих масштабах цей підхід не є автоматично кращим варіантом.
Нехай розмір команди визначає рішення
Питання, яке рідко з’являється на діаграмах архітектури, — це кількість інженерів, які у вас насправді є. Наведені нижче діапазони є гіпотезами, заснованими на тому, як команди зазвичай описують свої рішення пізніше, а не жорсткими правилами:
- Менше приблизно 15 інженерів: модульний моноліт майже завжди є кращим варіантом. Не вистачає людей, щоб обґрунтувати окремі процеси розгортання.
Переконливе обґрунтування вибору архітектури
На вищих посадах очікують рішення, а не перелік варіантів. Хороша відповідь зазвичай складається з чотирьох частин:
- Що ви використовуєте. Наприклад: сторінки маркетингу генеруються статично, панелі керування використовують SSR, а BFF знаходиться перед мобільним додатком.
Останній пункт має найбільше значення. Пояснення того, що ви навмисно не обрали, — це те, що відрізняє просте перелічення варіантів від прийняття рішення. Прикладом може слугувати зміна підходу до відображення контенту: навіть платформа, яка сприяла цьому методу, змінила свою політику, коли результати вимірювань суперечили один одному.
Поширені запитання
У чому різниця між SSR та SSG?
Оскільки SSR генерує вміст при кожному запиті, він може містити актуальні дані або дані, прив’язані до конкретного користувача. SSG створює HTML один раз під час процесу компіляції та надає статичні файли, що робить процес швидшим та дешевшим, але результат буде актуальним лише до наступної версії. ISR знаходиться між ними, генеруючи окремі сторінки за таймером.
Чи варто використовувати BFF, якщо є лише один фронтенд?
Зазвичай ні. BFF стає доцільним, коли кілька клієнтів — наприклад, мобільні пристрої, веб-сайти та API партнерів — потребують суттєво різних даних від одного бекенду. З одним фронтендом це здебільшого призводить до додаткової передачі даних через мережу та необхідності підтримки ще одного сервісу.
Чи мертве технологія edge rendering?
Ні, але стандарт змінився. Надання статичних шеллів з краю мережі все ще залишається корисним, а платформи типу Cloudflare Workers демонструють високу ефективність, коли дані розподілені по всьому світу. Для типових додатків, які підтримуються базою даних у одному регіоні, тепер загальноприйнятим правилом є розміщення обчислювальних ресурсів ближче до даних, а не до користувача.
Коли команді варто перейти на мікро-фронтенди?
Коли координація випуску між командами стає справжньою перешкодою, а не раніше. Складний додаток, який належить одній команді, отримує ті самі організаційні переваги від модульного моноліту, але з значно меншими витратами.
Основні висновки
- Кожен з цих підходів є відповіддю на одне запитання: яка частина роботи належить браузеру, серверу та етапу створення.