Gdzie w naszych aplikacjach powinniśmy wdrożyć logikę i renderowanie? Czy powinniśmy korzystać z renderowania po stronie serwera? A nawodnienie organizmu? Znajdźmy odpowiedzi.
Jako programiści często musimy podejmować decyzje, które wpłyną na całą architekturę naszych aplikacji. Jedną z najważniejszych decyzji, które muszą podjąć programiści stron internetowych, jest to, gdzie w aplikacjach zaimplementować logikę i renderowanie. Może to być trudne, ponieważ istnieje wiele różnych sposobów tworzenia witryny.
Nasze zrozumienie tego zagadnienia bazuje na wynikach naszej pracy w Chrome, rozmawianej z dużymi witrynami w ciągu ostatnich kilku lat. Ogólnie rzecz biorąc, zalecamy, aby deweloperzy rozważali renderowanie po stronie serwera lub renderowanie statyczne zamiast pełnego nawodnienia.
Aby lepiej zrozumieć systemy, na których opieramy się podczas podejmowania decyzji, musimy dobrze rozumieć poszczególne podejścia i spójnie stosować spójną terminologię. Różnice między tymi podejściami pokazują kompromisy związane z renderowaniem w internecie przez pryzmat wydajności.
Terminologia
Renderowanie
- Renderowanie po stronie serwera (SSR): renderowanie po stronie klienta lub aplikacji uniwersalnej w formacie HTML na serwerze.
- Renderowanie po stronie klienta (CSR): renderowanie aplikacji w przeglądarce przy użyciu JavaScriptu w celu modyfikacji DOM.
- Ponowne nawodnienie: „uruchamianie” widoków JavaScript na kliencie w taki sposób, że ponownie wykorzystuje drzewo DOM i dane HTML wyrenderowanego przez serwer.
- Renderowanie wstępne: uruchomienie aplikacji po stronie klienta podczas kompilacji w celu przechwycenia jej stanu początkowego w postaci statycznego kodu HTML.
Skuteczność
- Czas do pierwszego bajtu (TTFB): czas upływający między kliknięciem linku a wyświetleniem pierwszego fragmentu treści.
- Pierwsze wyrenderowanie treści (FCP): czas, kiedy żądana treść (treść artykułu itp.) staje się widoczna.
- Interakcja z kolejnym wyrenderowaniem (INP): to reprezentatywne dane, które pokazują, czy strona stale reaguje na dane wejściowe użytkownika.
- Całkowity czas blokowania (TBT): wskaźnik serwera proxy dla INP, który oblicza czas blokowania wątku głównego podczas wczytywania strony.
Renderowanie po stronie serwera
Renderowanie po stronie serwera w odpowiedzi na nawigację generuje pełny kod HTML strony na serwerze. Pozwala to uniknąć dodatkowych transferów danych w obie strony i tworzenia szablonów po stronie klienta, ponieważ proces ten odbywa się przed otrzymaniem odpowiedzi przez przeglądarkę.
Renderowanie po stronie serwera zazwyczaj generuje szybki FCP. Uruchomienie logiki strony i renderowanie stron na serwerze pozwala uniknąć wysyłania dużej ilości kodu JavaScript do klienta. Pomaga to zmniejszyć ilość TBT strony, która może również obniżyć wartość INP, ponieważ wątek główny nie jest blokowany tak często podczas wczytywania strony. Gdy wątek główny jest rzadziej blokowany, interakcje użytkowników będą miały więcej okazji na szybsze uruchomienie. Ma to sens, ponieważ w przypadku renderowania po stronie serwera wysyłasz po prostu tekst i linki do przeglądarki użytkownika. To podejście może się sprawdzić w przypadku szerokiego spektrum warunków dotyczących urządzenia i sieci oraz otwiera interesujące optymalizacje przeglądarki, takie jak analiza strumieniowego przesyłania dokumentów.

Dzięki renderowaniu po stronie serwera użytkownicy mogą rzadziej czekać na uruchomienie kodu JavaScript powiązanego z CPU, zanim będą mogli skorzystać z Twojej witryny. Nawet jeśli nie można uniknąć uniknięcia stosowania kodów JavaScript innych firm, korzystanie z renderowania po stronie serwera w celu ograniczenia własnych kosztów JavaScriptu może pozwolić na zwiększenie budżetu na pozostałe. Istnieje jednak jeden możliwy wadliwe działanie tej metody: generowanie stron na serwerze wymaga czasu, co może skutkować większą liczbą wyświetleń TTFB.
To, czy renderowanie po stronie serwera jest wystarczające do danej aplikacji, w dużej mierze zależy od tego, jakie środowisko tworzysz. Od dawna toczy się debata na temat właściwego zastosowania renderowania po stronie serwera w porównaniu z renderowaniem po stronie klienta, ale trzeba pamiętać, że tylko w przypadku niektórych stron można skorzystać z renderowania po stronie serwera. W niektórych witrynach z powodzeniem zastosowano techniki renderowania hybrydowego. Serwer Netflix renderuje stosunkowo statyczne strony docelowe, a jednocześnie pobiera z wyprzedzeniem kod JavaScript w przypadku stron intensywnie korzystających z interakcji, co zwiększa prawdopodobieństwo szybkiego wczytywania takich cięższych stron renderowanych przez klienta.
Wiele nowoczesnych platform, bibliotek i architektur umożliwia renderowanie tej samej aplikacji zarówno po stronie klienta, jak i po stronie serwera. Te techniki można stosować do renderowania po stronie serwera. Warto jednak pamiętać, że architektury, w których renderowanie odbywa się zarówno na serwerze, jak i po stronie klienta, to odrębna klasa rozwiązań o zupełnie odmiennych parametrach wydajności i spadkach. Użytkownicy React mogą używać serwerowych interfejsów API DOM lub rozwiązań utworzonych na ich podstawie, np. Next.js do renderowania po stronie serwera. Użytkownicy Vue mogą przeczytać przewodnik po renderowaniu po stronie serwera Vue lub Nuxt. Angular oferuje Universal. Większość popularnych rozwiązań korzysta z pewnej formy nawodnienia, więc zanim wybierzesz narzędzie, zapoznaj się z podaną metodą.
Renderowanie statyczne
Renderowanie statyczne ma miejsce podczas kompilacji. To podejście zapewnia szybki FCP i niższy koszt TBT oraz INP – zakładając, że ilość JS po stronie klienta jest ograniczona. W przeciwieństwie do renderowania po stronie serwera udało się również osiągnąć niezmiennie szybkie zmiany TTFB, ponieważ kod HTML strony nie musi być generowany dynamicznie na serwerze. Ogólnie renderowanie statyczne polega na utworzeniu z wyprzedzeniem osobnego pliku HTML dla każdego adresu URL. Dzięki generowanym z wyprzedzeniem odpowiedziom HTML renderowanie statyczne można wdrożyć w wielu sieciach CDN, aby wykorzystać buforowanie brzegowe.

Rozwiązania do renderowania statycznego są dostępne w różnych kształtach i rozmiarach. Narzędzia takie jak Gatsby zostały zaprojektowane tak, aby deweloperzy mieli wrażenie, że ich aplikacja jest renderowana dynamicznie, a nie generowana jako etap kompilacji. Narzędzia do generowania statycznych witryn, np. 11ty, Jekyll i Metalsmith, wykorzystują swój statyczny charakter, zapewniając podejście oparte na szablonach.
Jedną z wad renderowania statycznego jest to, że dla każdego możliwego adresu URL trzeba wygenerować osobne pliki HTML. Może to być trudne, a nawet niewykonalne, jeśli nie możesz z wyprzedzeniem przewidzieć, jakie adresy URL będą działać, lub w przypadku witryn z dużą liczbą unikalnych stron.
Użytkownicy React mogą znać mechanizm Gatsby, eksport statyczny Next.js lub Navi – wszystkie te funkcje ułatwiają tworzenie stron za pomocą komponentów. Trzeba jednak zrozumieć różnicę między renderowaniem statycznym a wstępnym renderowaniem: strony renderowane statyczne są interaktywne i nie wymagają wykonywania dużej ilości kodu JavaScript po stronie klienta. Natomiast renderowanie wstępne poprawia FCP aplikacji na jednej stronie, którą trzeba uruchomić po stronie klienta, aby była naprawdę interaktywna.
Jeśli nie masz pewności, czy dane rozwiązanie to renderowanie statyczne czy wstępne renderowanie, wyłącz JavaScript i wczytaj stronę, którą chcesz przetestować. W przypadku stron renderowanych statycznie większość funkcji będzie działać bez włączonej obsługi JavaScriptu. Wstępnie renderowane strony mogą nadal mieć niektóre podstawowe funkcje, takie jak linki, ale większość strony pozostanie bezczynna.
Innym przydatnym testem jest użycie ograniczania sieci w Narzędziach deweloperskich w Chrome i obserwowanie ilości pobranego kodu JavaScript, zanim strona stanie się interaktywna. Renderowanie wstępne zwykle wymaga większej ilości kodu JavaScript, aby stał się interaktywny. Jest on zwykle bardziej złożony niż metoda progresywnego ulepszania stosowana w renderowaniu statycznym.
Renderowanie po stronie serwera a renderowanie statyczne
Renderowanie po stronie serwera nie jest cudownym rozwiązaniem – jego dynamiczna charakter może się wiązać z znacznymi kosztami mocy obliczeniowej. Wiele rozwiązań renderujących po stronie serwera nie usuwa się wcześnie, może opóźnić TTFB lub podwoić ilość wysyłanych danych (np. stan w tekście używany przez JavaScript po stronie klienta). W reakcji na interfejs renderToString()
interfejs renderToString()
może działać wolniej, ponieważ jest synchroniczny i jednowątkowy. Nowsze interfejsy DOM API serwera React obsługujące strumieniowanie, które mogą szybciej uzyskać początkową część odpowiedzi HTML do przeglądarki, podczas gdy reszta jest nadal generowana na serwerze.
Poprawne renderowanie po stronie serwera może wymagać znalezienia lub utworzenia rozwiązania do buforowania komponentów, zarządzania wykorzystaniem pamięci, zastosowania technik zapamiętywania i innych problemów. Ta sama aplikacja jest zwykle przetwarzana lub odbudowywana wielokrotnie – raz na kliencie, a raz na serwerze. To, że renderowanie po stronie serwera może sprawić, że coś pojawi się szybciej, nie oznacza nagle, że będziesz mieć mniej do zrobienia. Jeśli po otrzymaniu odpowiedzi HTML wygenerowanej przez serwer na kliencie masz dużo pracy, nadal może to prowadzić do zwiększenia wartości TBT i INP dla Twojej witryny.
Renderowanie po stronie serwera generuje kod HTML na żądanie dla każdego adresu URL, ale może działać wolniej niż samo wyświetlanie statycznej treści renderowanej. Jeśli możesz poświęcić dodatkowe czynności, renderowanie po stronie serwera i buforowanie kodu HTML może znacznie skrócić czas renderowania na serwerze. Wadą renderowania po stronie serwera jest możliwość pobrania większej ilości „aktywnych” danych i reagowania na pełniejszy zestaw żądań niż jest to możliwe w przypadku renderowania statycznego. Strony wymagające personalizacji to konkretny przykład typu żądania, który nie sprawdza się w przypadku renderowania statycznego.
Renderowanie po stronie serwera może też dawać ciekawe decyzje podczas tworzenia aplikacji PWA: czy lepiej korzystać z pamięci podręcznej skryptu service worker zajmującej całą stronę, czy tylko z renderowania poszczególnych elementów treści na serwerze?
Renderowanie po stronie klienta
Renderowanie po stronie klienta oznacza renderowanie stron bezpośrednio w przeglądarce za pomocą kodu JavaScript. Wszystkie funkcje logiczne, pobieranie danych, szablony i routing są obsługiwane po stronie klienta, a nie serwera. Efektem jest to, że serwer przekazuje do urządzenia użytkownika więcej danych, co wiąże się z pewnymi kompromisami.
Renderowanie po stronie klienta może być trudne do pobrania i przyspieszenia działania na urządzeniach mobilnych. Jeśli wymaga to minimalnego nakładu pracy, renderowanie po stronie klienta może zbliżyć się do wydajności czystego renderowania po stronie serwera, przy zachowaniu ograniczonego budżetu JavaScript i dostarczania korzyści w jak najmniejszej liczbie cykli wymiany danych. Krytyczne skrypty i dane można dostarczyć wcześniej za pomocą metody <link rel=preload>
, która przyspiesza działanie parsera. Warto też ocenić wzorce takie jak PRPL, aby pierwsza i kolejna nawigacja sprawiała wrażenie natychmiastowej.

Podstawową wadą renderowania po stronie klienta jest to, że ilość wymaganego JavaScriptu rośnie wraz z rozwojem aplikacji, co może mieć negatywny wpływ na wartość INP strony. Jest to szczególnie trudne w przypadku dodania nowych bibliotek JavaScript, elementów polyfill i kodu zewnętrznego, które konkurują o moc obliczeniową i często muszą zostać przetworzone, zanim zawartość strony będzie mogła zostać wyrenderowana.
W środowiskach, które korzystają z renderowania po stronie klienta i korzystają z dużych pakietów JavaScriptu, warto zastosować agresywny podział kodu w celu zmniejszenia liczby TBT i INP podczas wczytywania strony. Pamiętaj też o leniwym ładowaniu kodu JavaScript, ponieważ „wyświetlaj tylko to, czego potrzebujesz, wtedy, gdy tego potrzebujesz”. W przypadku rozwiązań z małą interaktywność lub brakiem interaktywności renderowanie po stronie serwera może być lepszym rozwiązaniem dla tych problemów.
Osoby tworzące aplikacje jednostronicowe stwierdzą, że główne części interfejsu są wykorzystywane przez większość stron. Dzięki temu można zastosować metodę buforowania powłoki aplikacji. W połączeniu z skryptami service worker może to znacznie poprawić wydajność w przypadku powtarzających się wizyt, ponieważ kod HTML powłoki aplikacji i jego zależności można bardzo szybko ładować z pliku CacheStorage
.
Łączenie renderowania po stronie serwera i renderowania po stronie klienta w ramach nawodnienia
To podejście ma na celu zniwelowanie kompromisów między renderowaniem po stronie klienta a renderowaniem po stronie serwera. Żądania nawigacyjne, takie jak pełne wczytanie strony lub jej ponowne wczytanie, są obsługiwane przez serwer, który renderuje aplikację w formacie HTML, a następnie kod JavaScript i dane używane do renderowania są osadzone w wynikowym dokumencie. Starannie wykonane podejście pozwala uzyskać szybki FCP, tak jak w przypadku renderowania po stronie serwera, a następnie „odtwarza” ponownie, renderowanie na kliencie z wykorzystaniem techniki zwanej (re)hydratacja. Jest to skuteczne rozwiązanie, które jednak może mieć wiele wad w zakresie wydajności.
Podstawową wadą renderowania po stronie serwera z nawodnictwem jest to, że może ono mieć znaczny negatywny wpływ na TBT i INP, nawet jeśli poprawia FCP. Strony renderowane po stronie serwera mogą wydawać się interaktywne i wczytywane, ale nie mogą reagować na dane wejściowe, dopóki nie zostaną wykonane skrypty komponentów i dołączone moduły obsługi zdarzeń. Na urządzeniu mobilnym może to potrwać kilka sekund, a nawet minuty.
Być może przydarzyło Ci się to samo – przez jakiś czas po wczytaniu strony, kliknięciu lub dotknięciu nic nie dało. Szybko jest to frustrujące, ponieważ użytkownik nie może wiedzieć, dlaczego próbując wejść w interakcję ze stroną, nic się nie dzieje.
Problem z nawodnieniem: jedna aplikacja w cenie dwóch
Problemy z nawodnictwem mogą być często gorsze niż opóźniona interaktywność spowodowana przez JavaScript. Aby JavaScript po stronie klienta mógł dokładnie „rozpocząć” go w miejscu, w którym serwer został przerwany, bez konieczności ponownego żądania wszystkich danych używanych przez serwer do renderowania kodu HTML, obecne rozwiązania renderujące po stronie serwera zserializują odpowiedź z zależności danych interfejsu użytkownika do dokumentu jako tagi skryptu. Powstały dokument HTML zawiera wysoki poziom powielenia:

Jak widać, serwer w odpowiedzi na żądanie nawigacji zwraca opis interfejsu aplikacji, ale zwraca też dane źródłowe użyte do utworzenia tego interfejsu oraz pełną kopię implementacji UI, która następnie uruchamia się po stronie klienta. Ten interfejs użytkownika staje się interaktywny dopiero po zakończeniu wczytywania i uruchomienia funkcji bundle.js
.
Dane o wydajności prawdziwych witryn uzyskane za pomocą renderowania po stronie serwera i nawodnienia wskazują, że nie zaleca się ich stosowania. Ostatecznie przyczynę sprowadza się do wygody użytkowników: bardzo łatwo jest zostawić ich w „niesamowitej dolinie”, w której interaktywność wydaje się brakować, chociaż strona wydaje się być gotowa.

Jest jednak nadzieja na renderowanie po stronie serwera z nawodnictwem. W najbliższej perspektywie korzystanie z renderowania po stronie serwera w przypadku treści, które można przechowywać w pamięci podręcznej, może ograniczyć efekt TTFB i uzyskać wyniki podobne do renderowania wstępnego. Nawodnienie organizmu przyrostowo, stopniowo lub częściowo może być kluczem do zwiększenia przydatności tej techniki w przyszłości.
Strumieniowe renderowanie po stronie serwera i progresywne nawodnienie
W ciągu ostatnich kilku lat wprowadzono wiele zmian w renderowaniu po stronie serwera.
Strumieniowe renderowanie po stronie serwera umożliwia wysyłanie kodu HTML w częściach, które przeglądarka może renderować stopniowo. Może to prowadzić do szybkiego FCP, ponieważ znaczniki są szybciej dostarczane do użytkowników. W reakcie strumienie są asynchroniczne w ciągu [renderToPipeableStream()
] (w porównaniu ze synchronicznymi renderToString()
), co oznacza, że obciążenie wsteczne jest dobrze obsługiwane.
Warto też zastanowić się nad stopniowym nawodnieniem, np. o procesie nawodnienia aplikacji React, który już trafił. Przy tym podejściu pojedyncze elementy aplikacji renderowanej przez serwer są „uruchamiane” wraz z upływem czasu, a nie jak dotychczas powszechne inicjowanie całej aplikacji od razu. Pomaga to zmniejszyć ilość kodu JavaScript wymaganego do zapewnienia interaktywności stron, ponieważ aktualizowanie części strony o niskim priorytecie po stronie klienta może zostać odroczone, aby zapobiec blokowaniu wątku głównego, co pozwala na interakcje użytkowników wcześniej po ich zainicjowaniu przez użytkownika.
Stopniowe nawodnienie może również pomóc uniknąć jednej z najczęstszych problemów z nawadnianiem po stronie serwera, w którym drzewo DOM renderowane przez serwer zostaje zniszczone, a następnie natychmiast skompilowane. Często jest to spowodowane tym, że początkowe synchroniczne renderowanie po stronie klienta wymagało danych, które nie były w pełni gotowe, np. oczekiwano na rozwiązanie elementu Promise
.
Częściowe nawodnienie
Częściowe nawodnienie jest trudne do wdrożenia. To podejście jest rozwinięciem koncepcji nawodnienia progresywnego, w ramach którego analizowane są poszczególne elementy (komponenty/widoki/drzewa), które mają być nawadniane stopniowo, i które charakteryzuje brak interaktywności lub brak reaktywności. W przypadku każdej z tych przeważnie statycznych części odpowiedni kod JavaScript jest następnie przekształcany w obiektywne odniesienia i funkcje dekoracyjne, co zmniejsza ich zasięg po stronie klienta do niemal 0.
Podejście do częściowego nawodnienia niesie ze sobą pewne problemy i kompromisy. Stanowi to interesujące wyzwania w zakresie buforowania, a nawigacja po stronie klienta oznacza, że nie możemy zakładać, że kod HTML renderowany przez serwer w przypadku bezwładnych części aplikacji będzie dostępny bez pełnego wczytania strony.
Renderowanie trisomorficzne
Jeśli możesz skorzystać z skryptów service worker, być może zainteresuje Cię renderowanie „trisomorficzne”. Jest to technika, w której możesz wykorzystać renderowanie po stronie serwera w przypadku początkowych lub innych elementów nawigacyjnych (bez JavaScriptu), a potem poprosić mechanizm service worker o renderowanie kodu HTML na potrzeby elementów nawigacyjnych po jego zainstalowaniu. Dzięki temu komponenty i szablony z pamięci podręcznej są aktualne oraz umożliwiają korzystanie z nawigacji w stylu SPA na potrzeby renderowania nowych widoków w tej samej sesji. Ta metoda działa najlepiej, gdy można współużytkować ten sam kod szablonów i routingu między serwerem, stroną klienta i mechanizmem Service Worker.

Uwagi na temat SEO
Zespoły często biorą pod uwagę wpływ SEO przy wyborze strategii renderowania w internecie. Renderowanie po stronie serwera jest często wybierane, aby zapewnić „kompletny efekt”, który roboty mogą łatwo interpretować. Roboty mogą zrozumieć kod JavaScript, ale często istnieją ograniczenia związane z renderowaniem. Renderowanie po stronie klienta może działać, ale często nie bez dodatkowego testowania i pracy. Ostatnio warto rozważyć renderowanie dynamiczne, jeśli Twoja architektura w dużym stopniu opiera się na języku JavaScript po stronie klienta.
W razie wątpliwości możesz skorzystać z narzędzia do testowania optymalizacji mobilnej, ponieważ pozwala ono sprawdzić, czy wybrane przez Ciebie podejście spełnia Twoje oczekiwania. Zawiera on wizualny podgląd każdej strony z perspektywy robota Google, zserializowaną treść HTML (po wykonaniu JavaScriptu) i ewentualne błędy napotkane podczas renderowania.

Podsumowanie
Podejmując decyzję o podejściu do renderowania, zmierz i dowiedz się, czym są Twoje wąskie gardła. Zastanów się, czy możesz zastosować renderowanie statyczne lub renderowanie po stronie serwera. Nie ma nic złego w przesyłaniu kodu HTML z minimalną liczbą JavaScriptu, aby zapewnić interaktywność. Oto przydatna infografika przedstawiająca widmo serwer-klient:

Środki
Dziękujemy za opinie i inspirację:
Jeffrey Posnick, Houssein Djirdeh, Shubhie Panicker, Chris Harrelson i Sebastian Markbåge