Czas działania przeglądarki, serwera lub procesu budowania: mapa decyzyjna architektur frontendowych
Zobacz, jak SSG, SSR, streaming, komponenty serwerowe, BFF-y, renderowanie na brzegu, monolity modułowe oraz mikrofrontendy odpowiadają na jedno pytanie: gdzie powinna odbywać się praca?
Dyskusje na temat architektury, a szczególnie rozmowy kwalifikacyjne z doświadczonymi specjalistami od frontendu, rzadko biorą pod uwagę to, co zbudowałeś. Liczy się dla nich powód, dla którego to tak zbudowałeś, a „tak miało to być w zespole” nie jest odpowiedzią. Dobrą wiadomością jest to, że niemal każdy wzorzec architektury frontendu odpowiada na to samo podstawowe pytanie: ile pracy powinno odbyć się w przeglądarce, ile na serwerze i ile podczas procesu budowania? Gdy zobaczysz SSR, Server Components, backends-for-frontends oraz micro-frontends jako różne sposoby wyznaczenia tej granicy, wybór między nimi oraz uzasadnienie tego wyboru staje się znacznie prostszy.
Kilka słów na temat źródeł: opisane poniżej sytuacje w firmach pochodzą z publicznych artykułów inżynieryjnych oraz oficjalnej dokumentacji i są tak oznaczone.
Jak zmieniła się granica pomiędzy klientem a serwerem
Początkowa sieć internetowa była prosta. Pliki HTML statyczne ładowały się niemal natychmiast i rzadko zdarzało się, że coś pójdzie nie tak.
Następnie frameworki MVC po stronie serwera, takie jak Django, Rails i ASP.NET, zaczęły generować HTML przy każdej prośbie. Strony mogły teraz wyświetlać dane dynamiczne, ale każde kliknięcie oznaczało pełną ponowną ładowanie strony.
Aplikacje jednostronicowe stworzone za pomocą React, Vue lub Angular mocno przechyliły szalę w drugą stronę. Przeglądarka przejęła kontrolę nad routowaniem, stanem, walidacją, a czasami nawet autoryzacją. Rezultatem była interfejs użytkownika, który wydaje się natychmiastowy, ale tylko po jego załadowaniu. To określenie kryje w sobie pułapkę: duże pliki JavaScript, wolne pierwsze wyświetlenie oraz gorsza dostępność. Google faktycznie wykonuje skrypty JavaScript, jednak renderowanie może opóźnić indeksację dużych stron, a wiele innych robotów, w tym boty do przeglądania treści w mediach społecznościowych oraz liczne roboty oparte na sztucznej inteligencji, w ogóle nie uruchamiają skryptów. Treści, których nie mogą skutecznie zobaczyć, dla nich po prostu nie istnieją.
Z daleka historia architektury frontendu wygląda jak seria przejść pomiędzy cienkimi klientami, w których większość pracy wykonywa serwer, a grubymi klientami, w których tę pracę wykonuje przeglądarka. Każdy wzorzec opisany w dalszej części tego przewodnika stanowi kolejną pozycję dla tej granicy.
Backend-for-Frontend: kształtowanie oddzielnej API dla każdego klienta
W architekturze Backend-for-Frontend (BFF) zespół frontendu uruchamia własną, prostą usługę przed rzeczywistymi usługami backendu. Ta usługa pełni jedną funkcję: przekształca dane backendu dokładnie tak, jak tego potrzebuje dany klient.
To wzorzec wywodzi się zazwyczaj z SoundCloud. Około 2013 roku, podczas przekształcania monolitu w mikrosłużby, firma odkryła, że klienty webowe, iOS i Android konkurują ze sobą o jeden wspólny API, który nie odpowiadał żadnemu z nich. Każdy klient otrzymał własny, lekki backend. Phil Calçado, który brał udział w tej migracji, później szczegółowo opisał jej historię, a artykuł Sama Newmana przekształcił go w powszechnie cytowany opis wzorca, który dał mu nazwę. Netflix doszedł do podobnego rozwiązania niezależnie, wykorzystując warstwy adapterów specyficzne dla poszczególnych urządzeń, ponieważ aplikacja telewizyjna i aplikacja na telefon wymagają zupełnie innych danych.
Rozważmy konkretny przypadek. Aplikacja mobilna potrzebuje nazwy produktu, ceny oraz miniatury. Aplikacja internetowa wymaga dodatkowo recenzji, poziomów zapasów oraz powiązanych produktów. Jeden wspólny punkt końcowy albo wysyła aplikacji mobilnej zbyt wiele danych, albo zbyt mało aplikacji internetowej, w rezultacie czego zespoły muszą dodawać parametry zapytania, aby to skompensować.
BFF rozwiązuje ten problem, dostarczając każdej z aplikacji dostosowaną odpowiedź. Poniższa usługa Express, która korzysta z funkcji fetch wbudowanej w Node 18 i nowsze wersje, wywołuje API produktu i zwraca tylko cztery pola, zmieniając title na name oraz redukując listę obrazów do jednej miniatury:
// 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'));
Klient żąda adresu /products/123 i otrzymuje dokładnie taką strukturę, jakiej potrzebuje – bez dodatkowych pól i bez zbędnych przejść. W kodzie produkcyjnym należałoby również sprawdzić wartość response.ok przed analizą danych oraz zabezpieczyć się przed produktami bez obrazów, ponieważ data.images[0] zakłada istnienie co najmniej jednego obrazu.
Koszty związane z tym rozwiązaniem wymagają większej uwagi, niż zwykle im poświęca się. BFF to kolejna usługa, którą trzeba wdrożyć, monitorować i utrzymywać w dobrym stanie. Jeśli ona zawiedzie, awarii ulegnie również interfejs użytkownika, nawet jeśli prawdziwy backend funkcjonuje bez zarzutu. Nie jest to darmowa infrastruktura – stanowi dodatkowy punkt awarii, którego główną korzyścią jest większa wygoda dla zespołu frontendu. Aby dowiedzieć się więcej na temat tworzenia takiego rozwiązania w aplikacji Next.js, zapoznaj się z artykułem „Przekształcanie obsługiwaczy tras w Next.js w celową warstwę BFF”.
Strategie renderowania: gdzie powstaje HTML
W ostatnich latach renderowanie zmieniło się bardziej niż jakakolwiek inna dziedzina, a w praktyce kategorie te są bardziej nierozróżnialne, niż sugerują diagramy.
Generowanie statyczne i stopniowe odnowienie
Static Site Generation (SSG) renderuje każdą stronę podczas procesu budowania i serwuje proste pliki z CDN. Nie ma nic szybszego ani tańszego do serwowania, ale treść pozostaje nieruchoma aż do następnego wdrożenia.
Incremental Static Regeneration (ISR) wprowadza mechanizm regulacji: strona może zostać odbudowana w tle po upływie ustawionego interwału. Zachowujesz większość szybkości plików statycznych, nie musząc ponownie je wdrażać za każdym razem, gdy zmienia się treść.
Renderowanie po stronie serwera i strumieniowanie
Server-Side Rendering (SSR) generuje HTML dla każdej prośby. W swojej klasycznej formie serwer wysyła pełną stronę, a następnie przeglądarka ją odżywia: pobiera plik z JavaScriptem i dodaje obsługę zdarzeń oraz stan do elementów już widocznych na ekranie.
React 18 wprowadził streaming SSR za pomocą renderToPipeableStream, który wysyła fragmenty HTML zaraz po przygotowaniu każdej części struktury, zamiast czekać na naj wolniejszy komponent. Streaming jest standardowym wyborem dla nowych aplikacji, chociaż wiele aplikacji produkcyjnych nadal bez problemów korzysta z klasycznego SSR realizowanego jednorazowo.
Komponenty serwerowe, wyspy i możliwość kontynuacji
React Server Components (RSC) oraz architektury typu island idą o krok dalej: tylko interaktywne części strony wysyłają JavaScript. Treść statyczna pozostaje zwykłym HTML, który nie wymaga procesu hydratacji. architektury typu island w Astro stosują tę koncepcję poza Reactem. Qwik idzie jeszcze dalej dzięki możliwości kontynuacji pracy, która w dużej mierze unika procesu hydratacji poprzez serializację stanu aplikacji do HTML, dzięki czemu klient może kontynuować pracę tam, gdzie przerwał serwer.
Poniższy przykład pokazuje stronę składnika serwerowego w Next.js App Router. Od wersji Next.js 15 params to Promise, który musi zostać oczekiwany, dlatego składnik destrukturyzuje id dopiero po wywołaniu await params. Pobieranie danych odbywa się na serwerze, więc nazwa produktu i cena przychodzą już w postaci gotowego 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>
);
}
Tylko AddToCartButton zawiera JavaScript po stronie klienta. Aby tak było, musi znajdować się w osobnym pliku oznaczonym dyrektywą 'use client' i być importowany do strony; ze względu na zwięzłość fragment kodu pominął ten import. W rezultacie powstaje mały plik, który jest interaktywny tam, gdzie to istotne, a wszędzie indziej pozostaje statyczny.
Renderyzacja na Edge i dlaczego branża częściowo zmieniła kurs
Rendering na brzegu to jeden z najwyraźniejszych przykładów we współczesnym frontendzie idei testowanej publicznie i stale udoskonalanej.
Między około 2021 a 2023 rokiem argumentacja była przekonująca: należy wykonywać SSR na brzegu, na platformach takich jak Cloudflare Workers lub Vercel Edge Functions, w centrum danych znajdującym się blisko każdego użytkownika. Krótsza odległość oznacza szybsze strony. Vercel intensywnie promował ten podejście.
Vercel później otwarcie zmienił swoją decyzję; ówczesny wiceprezes ds. produktu opisał to jako „to mnie oszukało”. Powód jest pouczający. Obliczenia muszą znajdować się blisko użytkownika, ale jednocześnie blisko danych, a większość baz danych znajduje się w jednym regionie. Funkcja typu edge w Tokio, która dokonuje kilku przejazdów do bazy danych w Wirginii, często działa wolniej niż proste renderowanie w Wirginii. Gdy Vercel przeprowadził pomiarы na swoim własnym produkcie v0, okazało się, że zwykłe renderowanie przy użyciu Node.js jest szybsze niż renderowanie typu edge. Vercel następnie porzucił samodzielne funkcje Edge Functions i obecnie zaleca użycie środowiska Node.js, przy czym obliczenia muszą znajdować się w tym samym regionie co dane; dokładny stan każdego środowiska można sprawdzić w aktualnej dokumentacji platformy.
To, co przetrwało, to węższe podejście: natychmiast dostarczyć statyczną strukturę strony z brzegu, a następnie przesyłać dynamiczne elementy z serwerów znajdujących się obok danych. To właśnie robi Partial Prerendering; wyjaśnienie koncepcji partial pre-rendering i concurrent rendering opisuje zasady działania tego rozwiązania.
Cloudflare Workers pozostaje prawdziwą platformą SSR typu edge i dobrze funkcjonuje, gdy same dane są rozproszone na całym świecie. Ważną lekcją jest to, że lokalizacja danych zwykle ma większe znaczenie niż lokalizacja użytkownika. Zrozumienie powodów, dla których branża zmieniła swoje podejście, jest cenniejsze niż samo znanie tego terminu modnego w branży.
Modyfikalny monolit frontendu
Gdy aplikacja jednostronicowa rozwija się i obejmuje więcej niż kilka zespołów, prosty repozytorium staje się ryzykowny. Wszyscy edytują te same wspólne komponenty, a nikt nie wie dokładnie, kto jest odpowiedzialny za co.
Modularny monolit dzieli bazę kodu na dwie warstwy, zachowując jedną nadającą się do wdrożenia:
- Warstwę platformową, za którą odpowiada zespół platformy, zawierającą system projektowy, wspólne elementy, mechanizmy logowania oraz podobną infrastrukturę.
- Warstwę domenową składającą się z folderów funkcjonalnych, takich jak
user/lubpayments/, z których każdy jest w zarządzie odpowiedniego zespołu funkcjonalnego.
Motywacja ta przypomina architekturę czystą lub sześciokątną, bez większości jej formalności. Pełna architektura czysta jest zazwyczaj przesadą w frontendzie – przycisk i żądanie nie wymagają trzech warstw abstrakcji pomiędzy nimi. Ważne jest jasne rozdzielenie odpowiedzialności oraz ścisłe granice, na przykład reguły lintingu, które uniemożliwiają jednej domenie importowanie elementów wewnętrznych innej.
Mikrofrontendy: niezależność za cenę
Architektura mikrofrontendu traktuje każdą domenę jako oddzielnie deployowalną mini-aplikację, zwykle ładowaną w czasie wykonywania przez aplikację nadrzędną za pomocą mechanizmu takiego jak Webpack Module Federation.
To, co osiągasz, to prawdziwa autonomia: zespoły mogą publikować treści według własnego harmonogramu, a w razie rzeczywistej konieczności mogą nawet używać różnych frameworków. Zalando, IKEA i DAZN opisały swoje doświadczenia z wdrażaniem tego rozwiązania na dużą skalę, zawsze przy udziale dużych zespołów inżynierów oraz znacznych inwestycji w wspólne narzędzia. micro-frontends.org pozostaje standardowym źródłem informacji na ten temat.
Sposób awarii, który naprawdę szkodzi
Problem, który powraca w rzeczywistych raportach z incydentów, to nie mieszanie różnych frameworki. Chodzi o zmiany w wspólnych zależnościach. Jedna aplikacja zdalna aktualizuje wspólną bibliotekę, podczas gdy inna tego nie robi, w rezultacie na tej samej stronie uruchamiają się dwie kopie Reacta, które konkurują o ten sam DOM. Module Federation może służyć do deklarowania wspólnych singletonów oraz zakresów wersji, aby temu zapobiec, ale tylko wtedy, gdy zespoły się na tym zgadzają i przestrzegają tych ograniczeń. To właśnie ten konkretny problem koordynacji niszczy zespoły, znacznie bardziej niż często wymieniana „złożoność”.
Istnieje również przykład ostrzegawczy w przeciwnym kierunku. Podobno Spotify kilka lat temu eksperymentowało z podejściem mikrofrontendowym opartym na iframe w swoim kliencie desktopowym, a później przechodziło na zintegrowaną architekturę, częściowo dlatego, że połączenia między poszczególnymi elementami kosztowały więcej niż zyskana niezależność. Nawet w dużych skali to rozwiązanie nie gwarantuje automatycznego sukcesu.
Niech rozmiar zespołu decyduje
Pytanie, które rzadko pojawia się na diagramach architektury, brzmi: ilu dokładnie masz inżynierów. Poniższe zakresy to heurystyki wynikające z tego, jak zespoły zwykle opisują swoje decyzje później, a nie sztywne zasady:
- Mniej niż około 15 inżynierów: modularny monolit prawie zawsze jest lepszym wyborem. Nie ma wystarczająco wielu osób, aby uzasadnić oddzielne procesy wdrażania.
Konkretne uzasadnienie wyboru architektury
Osoby na stanowiskach kierowniczych oczekują decyzji, a nie listy opcji. Dobry odpowiedź zazwyczaj składa się z czterech części:
- To, co uruchamiasz. Na przykład: strony marketingowe są generowane statycznie, panele kontrolne wykorzystują SSR, a BFF znajduje się przed aplikacją mobilną.
Ostatni punkt ma największe znaczenie. Wyjaśnienie tego, czego celowo nie wybrano, odróżnia prostą wymianę informacji od podjęcia decyzji. Przykładem może być zmiana podejścia do renderowania na krańcach sieci – nawet platforma, która promowała tę metodę, zmieniła kurs, gdy wyniki pomiarów były sprzeczne.
Częste pytania
Jak różnią się SSR i SSG?
Ponieważ SSR renderuje treść przy każdej prośbie, może zawierać dane aktualne lub dostosowane do konkretnego użytkownika. SSG buduje plik HTML raz podczas procesu kompilacji i serwuje statyczne pliki, co jest szybsze i tańsze, ale dostępne tylko w wersji z najnowszą aktualizacją. ISR znajduje się pomiędzy nimi, regenerując poszczególne strony według ustalonego harmonogramu.
Czy BFF jest opłacalny przy tylko jednym interfejsie użytkownika?
Zazwyczaj nie. BFF ma sens wtedy, gdy kilka klientów, takich jak aplikacje mobilne, strona internetowa czy API partnera, wymaga zupełnie innych danych od tego samego backendu. Przy jednym interfejsie użytkownika dodaje on głównie jeden krok w sieci oraz kolejną usługę do obsługi.
Czy renderowanie na brzegu jest już przestarzałe?
Nie, ale domyślna konfiguracja się zmieniła. Dostarczanie statycznych plików z chmury brzegowej nadal ma wartość, a platformy typu Cloudflare Workers sprawdzają się najlepiej, gdy dane są rozproszone na całym świecie. W przypadku typowych aplikacji opartych na bazie danych z jednego regionu zasadą jest teraz umieszczanie zasobów obliczeniowych w pobliżu danych, a nie użytkowników.
Kiedy zespół powinien przenieść się na mikrofrontendy?
Gdy koordynacja wypuszczania aktualizacji pomiędzy zespołami staje się prawdziwym wąskim gardłem, a nie wcześniej. Złożona aplikacja należąca do jednego zespołu przynosi te same korzyści organizacyjne dzięki monolitowi modułowemu, przy znacznie mniejszym obciążeniu.
Główne wnioski
- Każdy z przedstawionych tu wzorców jest odpowiedzią na jedno pytanie: ile pracy należy do przeglądarki, serwera oraz etapu budowania aplikacji.