Strona główna / Artykuły

Jak inżynieria frontend rozwinęła się od stylizacji do skalowania systemów

Inżynieria frontend rozwinęła się od HTML i CSS do komponentów React, cache’owania, narzędzi do zarządzania monorepo oraz możliwości obserwacji. Co zmieniło się na każdym etapie i dlaczego tak się stało.

Wydawca: REACTAPP.TOP10 min czytania2240 słów

Źródła: I Thought Frontend Was Colors and Fonts. Then I Shipped to a Million Users.

Narzędzia się rozwinęły, ale nasze sposoby myślenia ewoluowały jeszcze bardziej.

Zapytaj kogoś, co oznacza rozwój front-endu, i zazwyczaj usłyszysz tę samą odpowiedź: HTML, CSS, JavaScript. Kolory, czcionki, obrazy, układ.

To jest rozsądny punkt wyjścia. Po prostu nie jest to koniec historii.

To, co zaczęło się od tworzenia stron internetowych, rozwinęło się w projektowanie pełnych systemów, które muszą pozostać szybkie, poprawne i nadal poddawane kontroli, gdy tysiące osób korzysta z nich jednocześnie.

Narzędzia ewoluowały w tym kierunku. Ale jeszcze większe zmiany zaszły w zestawie pytań, które nauczyliśmy się sobie zadawać.

Oto cała droga rozwoju.

1. Wszystko zaczęło się od HTML, CSS i JavaScript

Początkowa sieć internetowa utrzymywała wszystko w prostocie.

HTML zajmował się strukturą. CSS odpowiadał za prezentację. JavaScript dodawał element interaktywności.

Zadanie można streścić w jednym zdaniu:

Stwórz stronę, która dobrze wygląda i prawidłowo funkcjonuje.

Większość stron była statyczna. Serwery dostarczały kompletny kod HTML, przeglądarki go renderowały, a sama przeglądarka praktycznie nie zawierała żadnej logiki.

Z biegiem czasu strony przejęły większą liczbę obowiązków. Formularze zaczęły weryfikować wprowadzane dane w czasie rzeczywistym. Treść aktualizowała się bez konieczności pełnego ładowania strony.

JavaScript coraz częściej przejmował nowe funkcje i nigdy ich nie zwracał.

  1. Od stron do aplikacji

Strony internetowe przekształciły się w aplikacje. Oznaczało to interfejsy dynamiczne, wywołania API, formularze składające się z kilku kroków, procesy autoryzacji oraz dane przechowywane w przeglądarce pomiędzy poszczególnymi interakcjami.

Ta zmiana ujawniła problem, którego stare metody nie były przeznaczone do rozwiązania.

Gdy manipulujesz bezpośrednio DOM-em, widoczny ekran staje się twoim jedynym źródłem prawdy. Jeden element danych może istnieć w zmiennych, wewnątrz węzła DOM i w odpowiedzi od serwera. Nic nie gwarantuje, że te trzy elementy będą ze sobą zgodne.

Jeśli zaktualizujesz jeden z nich, a o pozostałych zapomnisz, w rezultacie pojawi się błąd, który ujawni się tylko przy określonej sekwencji kliknięć.

Potrzebna była metoda umożliwiająca określenie jak powinien wyglądać interfejs oraz przekazanie zadania ustalania konkretnych operacji DOM innemu elementowi.

3. React: myślenie w komponentach

Prawdziwym przełomem React nie była jego składnia, lecz model myślowy stojący za nią.

UI = f(state)

Opisujesz ekran dla danego stanu, a framework określa najmniejszy zestaw aktualizacji DOM potrzebny, aby do niego dotrzeć.

Zamiast pisać element.appendChild(...), należy napisać to, co powinno być prawdą.

Bazową jednostką pracy stał się już nie cała strona, lecz komponent. Może to być pojedynczy przycisk, karta przedstawiająca produkt, okno dialogowe lub jeden etap procesu zakupowego. Każdy z nich ma własną strukturę markup, własne stylizacje oraz własne zachowanie.

Oto co przyniosło taki podejście:

Możliwość ponownego użycia. Można stworzyć komponent raz i użyć go w wielu miejscach.

Rozumienie na poziomie lokalnym. Można zrozumieć jeden komponent bez konieczności przeglądania całego aplikacji.

Możliwość testowania. Wystarczy podać mu dane wejściowe i sprawdzić wynik – nie potrzeba przeglądarki.

Kompozowalność. Małe komponenty łączą się, tworząc większe.

Główne pytanie zmieniło się z "Jak stworzyć tę stronę?" na "Które elementy składają się na tę aplikację?"

4. Next.js: od interfejsu do architektury

Czysty React działający wyłącznie po stronie klienta miał swoje wady.

Brauzer najpierw ściąga pustą strukturę HTML. Następnie pobiera spory plik z kodem JavaScript. Potem go uruchamia. Dopiero wtedy ściąga rzeczywiste dane. Dopiero po tym wszystkim użytkownik widzi jakiś treść.

To jest w porządku na mocnym laptopie. Na telefonie średniej półki z słabym sygnałem oznacza to patrzenie na pustą ekran przez kilka sekund. A roboty wyszukiwawcze mogą ostatecznie nie indeksować niczego przydatnego.

Frameworki takie jak Next.js dają programistom kontrolę nad gdzie i kiedy faktycznie generowany jest HTML.

SSG. Renderowanie odbywa się podczas budowania aplikacji, a plik statyczny jest dostarczany z CDN. To najszybsza opcja, chociaż treść pozostaje niezmieniona aż do następnego budowania.

ISR. Treść pozostaje statyczna, ale jest regenerowana w tle po upływie określonego czasu. Otrzymujesz szybkość na poziomie plików statycznych przy jednoczesnym utrzymywaniu dość aktualnej treści.

SSR. Renderowanie odbywa się na serwerze dla każdej żądania. Treść jest zawsze aktualna, ale serwer musi wykonywać nowe obliczenia za każdym razem.

CSR. Renderowanie odbywa się wyłącznie w przeglądarce. Nadaje się to do ekranów wymagających zalogowania, gdzie indeksowanie nie ma znaczenia.

Kluczowa zasada: wybierasz metodę dla poszczególnych tras, a nie dla całej aplikacji naraz.

Strona marketingowa korzysta z SSG, strona produktu – z ISR, a panel sterowania – z CSR.

Tutaj również odgrywa rolę hydratacja. Serwer wysyła HTML, a następnie klient ponownie uruchamia drzewo komponentów w celu połączenia obsługowników zdarzeń. Wizualnie strona wygląda na gotową, zanim stanie się faktycznie interaktywna.

React Server Components oraz streaming istnieją głównie po to, by zamknąć tę lukę. HTML jest wysyłany natychmiast po jego dostępności, a mniej JavaScriptu trafia do części strony, które nie miały być interaktywne.

Na tym etapie praca nad frontendem przestaje być jedynie „warstwą interfejsu użytkownika”. Decyzje dotyczące strategii renderowania, routingu i cache’owania stają się decyzjami architektonicznymi, a to inżynierowie frontendu je podejmują.

5. Potem przyszła skalowalność

Budowa aplikacji to jeden z wyzwań. Budowa takiej, która obsługuje miliony użytkowników, to zupełnie inne wyzwanie.

Pytania, które zadajemy, zaczynają się zmieniać:

Co powinno się stać, gdy API poniżej w łańcuchu przetwarzania zwalnia, zamiast poprawnie zawieść?

Jak sześć oddzielnych zespołów może wprowadzać kod do tego samego repozytorium bez wzajemnych przeszkód?

Jak wdrożyć zmianę tylko dla 1% użytkowników i szybko ją cofnąć w ciągu minuty, jeśli coś pójdzie nie tak?

Jak aplikacja zachowuje się na trzyletnim telefonie z Androidem w miejscu, gdzie nigdy jej nie testowano?

Które z wdrożeń z wczoraj jest przyczyną dzisiejszego wzrostu liczby błędów?

To są pytania na poziomie systemu. Po prostu pojawiają się w przeglądarce.

6. Szybkość to nie dodatkowy element wyglądu, to podstawowe oczekiwanie

Elegancki interfejs, który potrzebuje sześciu sekund na renderowanie, może być uważany za uszkodzony. Użytkownicy odczuwają opóźnienie, zanim w ogóle będą w stanie określić, co jest nie tak.

Dzisiejsza dyscyplina w zakresie optymalizacji wydajności opiera się na pomiarach, a nie intuicji. Core Web Vitals dostarczają zespołom wspólnego języka do omawiania szybkości działania strony.

LCP (Largest Contentful Paint) oznacza moment, w którym główny treść staje się widoczna. Celuj w czas realizacji poniżej 2,5 sekundy.

INP (Interaction to Next Paint) pokazuje, jak szybko interfejs reaguje po naciśnięciu przycisku lub kliknięciu. Celuj w czas poniżej 200 milisekund.

CLS (Cumulative Layout Shift) mierzy, jak bardzo elementy na stronie się poruszają podczas jej ładowania. Celuj w wynik poniżej 0,1.

Każda technika optymalizacyjna odpowiada jednej z tych metryk.

Dzielenie kodu i jego ładowanie w trybie opóźnionym. Wyślij tylko tę część JavaScriptu, która jest rzeczywiście potrzebna na danej stronie, a nie cały plik aplikacji. Dzięki temu poprawia się zarówno LCP, jak i INP.

Dostosowywanie obrazów. Używaj nowoczesnych formatów, dostosuj ich rozmiar odpowiednio i zawsze podawaj szerokość oraz wysokość na wstępie, aby nic nie przesuwało się nagle. To pomaga poprawić wskaźniki LCP i CLS.

Rozsądne zarządzanie czcionkami. Załaduj je z góry, zastosuj font-display: swap oraz ogranicz zestaw znaków do tych, które faktycznie są używane. To pomaga w rozwiązaniu problemu CLS.

Dystrybucja przez CDN. Serwowanie danych z serwera znajdującego się geograficznie blisko użytkownika poprawia niemal wszystkie wskaźniki jednocześnie.

Zachowywanie obciążających zadań poza głównym wątkiem. Przeglądarka ma jeden wątek, który zarządza zarówno renderowaniem, jak i twoimi skryptami. Zadanie trwające 300 milisekund zamyka każdy kliknięcie przez cały ten czas. Stąd pochodzą korzyści wynikające z INP.

Zasada leżąca u podstaw, wyrażona prosto:

Zapewnij najlepszą jakość użytkowania przy użyciu jak najmniejszej ilości zasobów.

Jedna rada: dostosowuj się na podstawie rzeczywiście uzyskanych pomiarów, a nie założeń. Liczby zebrane z laptopa programisty w warunkach laboratoryjnych często różnią się od tego, co doświadczają prawdziwi użytkownicy w praktyce, a to właśnie dane z terenu odzwierciedlają rzeczywistość.

7. Cacheowanie: unikaj ponownego obliczania tego, co już rozwiązałeś

Gdy ruch rośnie, wielokrotne generowanie tej samej odpowiedzi staje się marnotrawstwem zasobów. Cacheowanie istnieje po to, by nie płacić dwukrotnie za identyczną pracę.

Każda prośba przechodzi przez kilka warstw, a każda z nich stanowi możliwość skrócenia procesu.

Browser → CDN → Application / ISR → Redis → Database

Im bliżej użytkownika jest źródło dostarczania odpowiedzi, tym jest ona tańsza i szybsza.

Jednak im bliżej użytkownika znajduje się coś w pamięci cache, tym trudniej jest cofnąć się, jeśli zawiera błędy.

To napięcie definiuje całą tę praktykę.

Czym powinna być wypełniona pamięć cache? Danymi stabilnymi, udostępnianymi między użytkownikami i wymagającymi dużych nakładów na ich generowanie. Nigdy niczym specyficznym dla danego użytkownika, chyba że klucz cache uwzględnia indywidualnego użytkownika.

Jak długo powinna istnieć? Krótkie okna wygaśnięcia są bezpieczniejsze, ale przynoszą mniejsze korzyści. Dłuższe okna działają szybciej, ale niosą większe ryzyko.

Jak ją wyczyścić? Możliwościami są wygaśnięcie oparte na czasie (TTL), usuwanie spowodowane zdarzeniami (aktywowane przy publikacji) lub haszowanie nazw plików na podstawie ich treści, dzięki czemu unieważnianie nie jest nawet konieczne.

Co się dzieje, gdy wyszukiwanie zawodzi? Jeśli klucz powszechnie używany wygaśnie, a lawina żądań dotrze jednocześnie do serwera backendowego, powstaje chaos. Chronisz się przed tym za pomocą mechanizmu blokującego lub poprzez dostarczanie przestarzałej wersji, podczas gdy pojedyncze żądanie zajmuje się jej odświeżeniem.

Wzorzec, który warto zapamiętać, to stale-while-revalidate. Natychmiast zwracaj wersję z pamięci cache, cicho odśwież ją w tle, a następny użytkownik otrzyma zaktualizowany treść. Użytkownicy doświadczają szybkości, a ty zapewniasz poprawność z krótkim opóźnieniem.

Zwróć uwagę na to, ile rzeczywiście kosztuje tu błąd. Uszkodzona pamięć cache nie tylko spowalnia działanie — może też pokazać błędną cenę, błędne imię lub zupełnie obce dane.

Kasowanie w pamięci cache wygląda jak technika optymalizacji wydajności, ale w rzeczywistości służy do zapewnienia poprawności.

8. Strukturyzacja dla rozwoju

Gdy w jednym repozytorium kodu pracuje kilka zespołów, sposób jego organizacji staje się równie ważny jak szybkość działania.

Grenice między modułami. Należy zdecydować, co jest dostępne publicznie, co pozostaje wewnętrzne oraz jakie zależności są dozwolone.

Umowy API. Muszą mieć typowanie i wersjonowanie, aby zmiana w warstwie backend nie zakłócała procesu budowania, a jedynie działanie strony online.

Gdzie znajduje się stan. Stan serwera, stan URL oraz lokalny stan klienta to zasadniczo odrębne kategorie. Większość błędów związanych ze stanem wynika z traktowania jednej kategorii jak drugiej.

Izolacja awarii. Granice błędów zapewniają, że jeden uszkodzony element nie spowoduje awarii całej strony.

wdrażanie zmian stopniowo. Flagi funkcji, wdrożenia typu canary oraz możliwość natychmiastowego cofnięcia zmian.

Sposób myślenia zmienia się z prostego sprawdzenia, czy dana funkcja działa, na upewnienie się, że nie utrudnia ona przyszłych modyfikacji.

9. Kod współdzielony w monorepo

Gdy kilka aplikacji korzysta z tych samych plików nagłówkowych, tych samych tokenów projektowych oraz tych samych narzędzi autoryzacji, kopiowanie i wklejanie kodu przestaje być rozwiązaniem zdatnym do dalszego stosowania.

Monorepo umożliwia zespołom współdzielenie komponentów interfejsu użytkownika, systemów projektowych, funkcji pomocniczych, konfiguracji oraz definicji typów – wszystko w ramach jednego kanonicznego źródła prawdy, przy czym zmiany są wprowadzane atomowo we wszystkich pakietach.

Jednak współdzielenie ma swoją cenę. Każdy współdzielony pakiet staje się zależnością, a każda zależność jest w praktyce kolejką: aktualizacja jednego zespołu może opóźnić wydanie produktu przez inny zespół.

Rzeczywistym celem jest ponowne wykorzystanie bez ścisłego powiązania.

Korzystna heurystyka: udostępniaj elementy, które rzadko się zmieniają i oznaczają to samo niezależnie od miejsca ich użycia — takie jak tokeny projektowe, komponent Button czy definicje typów.

Mikrofrontendy rozwijają tę ideę, umożliwiając zespołom niezależną pracę, ale kosztem większej złożoności w czasie wykonywania i powtarzających się zależności. Taka kompromis może mieć sens w dużej organizacji z wyraźnie określonymi granicami między zespołami. Poza tym kontekstem rzadko się opłaca.

10. Obserwowanie systemu w produkcji

W dużych skali zwroty takie jak „działa na moim komputerze” czy „działało dobrze w środowisku testowym” tracą na znaczeniu.

To, czego naprawdę potrzebujesz, to możliwość obserwowania zachowania aplikacji w środowisku produkcyjnym, na rzeczywistych urządzeniach, w czasie rzeczywistym.

Śledzenie błędów w połączeniu z przesłanymi mapami źródłowymi, dzięki czemu ślad wywołań wskazuje na twojego rzeczywistego kod źródłowego, a nie na skompresowane fragmenty.

Nadzór nad rzeczywistymi użytkownikami umożliwiający pobieranie danych Core Web Vitals od prawdziwych użytkowników, podzielonych według urządzenia, kraju i jakości połączenia.

Strukturyzowane logowanie powiązane z identyfikatorem żądania, który można śledzić od przeglądarki aż do każdej usługi backendowej, z którą ma ono kontakt.

Ostrzeżenia powiązane z konkretnymi objawami – informuj ludzi, gdy wskaźniki błędów lub opóźnień przekroczą określony próg, a nie tylko wtedy, gdy zużycie CPU serwera gwałtownie wzrośnie.

Śledzenie wersji. Oznacz każdą błąd konkretną wersją aplikacji, która go spowodowała, dzięki czemu odpowiedź na pytanie „co się zmieniło?” zajmuje sekundy, a nie godziny.

Pytania, na które ta metoda powinna umożliwić odpowiedź:

Gdzie dokładnie użytkownicy napotykają błędy? Która wywołanie API pogarsza jakość użytkowania? Która ścieżka jest wolna i dla jakiej grupy użytkowników? Czy najnowsza wersja aplikacji pogorszyła sytuację w porównaniu z poprzednimi wersjami?

Kod frontendu był kiedyś najtrudniejszą częścią całego stacku do obserwacji. Już nie musi tak być.

Literatura pokrewna