Renderowanie w internecie

Jednym z kluczowych wyborów, które muszą podjąć deweloperzy stron internetowych, jest miejsce implementacji logiki i renderowania w aplikacji. Może to być trudne, ponieważ istnieje wiele sposobów tworzenia witryn.

Nasze zrozumienie tej dziedziny zostało pogłębione dzięki naszej pracy nad Chrome w ostatnich latach, w której rozmawialiśmy z przedstawicielami dużych witryn. Ogólnie rzecz biorąc, zachęcamy deweloperów do korzystania z renderowania po stronie serwera lub renderowania statycznego zamiast pełnej hydratacji.

Aby lepiej zrozumieć architektury, spośród których wybieramy, podejmując tę decyzję, musimy dobrze poznać każdą z nich i ujednolicić terminologię, której będziemy używać. Różnice między metodami renderowania pomagają zrozumieć kompromisy związane z renderowaniem w internecie z perspektywy wydajności strony.

Terminologia

Najpierw zdefiniujemy terminologię, której będziemy używać.

renderowanie,

Renderowanie po stronie serwera (SSR)
Renderowanie aplikacji na serwerze, aby wysyłać do klienta kod HTML zamiast kodu JavaScript.
Renderowanie po stronie klienta (CSR)
Wyświetlanie aplikacji w przeglądarce z modyfikacją DOM za pomocą JavaScriptu.
nawadnianie;
„Uruchamianie” widoków JavaScript na kliencie, aby ponownie używać drzewa DOM i danych HTML renderowanego na serwerze.
Renderowanie wstępne
Uruchamianie aplikacji po stronie klienta w momencie kompilacji, aby uzyskać jej początkowy stan w postaci statycznego kodu HTML.

Wyniki

Czas do pierwszego bajtu (TTFB)
Czas od kliknięcia linku do załadowania pierwszego bajtu treści na nowej stronie.
Pierwsze wyrenderowane treści (FCP)
Czas, w którym żądane treści (tekst artykułu itp.) stają się widoczne.
Interakcja do kolejnego wyrenderowania (INP)
Przedstawia dane, które oceniają, czy strona szybko reaguje na dane wprowadzane przez użytkownika.
Całkowity czas blokowania (TBT)
Dane pośrednie dla INP, które obliczają, jak długo główny wątek był zablokowany podczas wczytywania strony.

Renderowanie po stronie serwera

Renderowanie po stronie serwera generuje pełny kod HTML strony na serwerze w odpowiedzi na nawigację. Dzięki temu można uniknąć dodatkowych połączeń na potrzeby pobierania danych i użycia szablonów po stronie klienta, ponieważ renderowanie odbywa się przed otrzymaniem odpowiedzi przez przeglądarkę.

Renderowanie po stronie serwera zwykle zapewnia szybkie wczytywanie FCP. Wykonywanie logiki strony i renderowanie na serwerze pozwala uniknąć wysyłania dużej ilości kodu JavaScript do klienta. Pomaga to zmniejszyć TBT strony, co może też prowadzić do niższego INP, ponieważ wątek główny nie jest tak często blokowany podczas wczytywania strony. Gdy wątek główny jest blokowany rzadziej, interakcje użytkowników mają większe szanse na wcześniejsze wykonanie. To ma sens, ponieważ w przypadku renderowania po stronie serwera wysyłasz tylko tekst i linki do przeglądarki użytkownika. Takie podejście może się sprawdzać w różnych warunkach dotyczących urządzenia i sieci, a także umożliwia ciekawe optymalizacje przeglądarki, takie jak parsowanie dokumentów strumieniowych.

Diagram przedstawiający renderowanie po stronie serwera i wykonywanie kodu JS wpływające na FCP i TTI
FCP i TTI z renderowaniem po stronie serwera.

Dzięki renderowaniu po stronie serwera użytkownicy nie muszą czekać na uruchomienie kodu JavaScript zależnego od procesora, aby móc korzystać z witryny. Nawet jeśli nie możesz uniknąć korzystania z kodu JavaScript firmy zewnętrznej, renderowanie po stronie serwera, które pozwala zmniejszyć własne koszty kodu JavaScript, może zwiększyć budżet na pozostałe elementy. Takie podejście ma jednak pewną wadę: generowanie stron na serwerze zajmuje czas, co może wydłużać czas TTFB strony.

To, czy renderowanie po stronie serwera wystarczy dla Twojej aplikacji, zależy głównie od tego, jakiego typu aplikację tworzysz. Od dawna trwa debata na temat prawidłowego stosowania renderowania po stronie serwera i po stronie klienta, ale zawsze możesz użyć renderowania po stronie serwera na niektórych stronach, a na innych nie. Niektóre witryny z powodzeniem stosują hybrydowe techniki renderowania. Na przykład Netflix renderuje na serwerze stosunkowo statyczne strony docelowe, a prefetching kod JS dla stron z dużą interakcją, co zwiększa szanse na szybkie wczytanie tych cięższych stron renderowanych po stronie klienta.

Wiele nowoczesnych frameworków, bibliotek i architektur umożliwia renderowanie tej samej aplikacji zarówno na kliencie, jak i na serwerze. Możesz stosować te techniki do renderowania po stronie serwera. Jednak architektury, w których renderowanie odbywa się zarówno na serwerze, jak i na kliencie, stanowią odrębną klasę rozwiązań o bardzo różnych parametrach i właściwościach. Użytkownicy React mogą używać serwerowych interfejsów API DOM lub rozwiązań opartych na nich, takich jak Next.js, do renderowania po stronie serwera. Użytkownicy Vue mogą skorzystać z przewodnika po renderowaniu po stronie serwera lub z Nuxt. Angular ma Universal. Większość popularnych rozwiązań wykorzystuje jednak jakąś formę nawilżania, więc pamiętaj o tym, z jakich metod korzysta Twoje narzędzie.

Renderowanie statyczne

Renderowanie statyczne odbywa się w czasie kompilacji. To podejście zapewnia szybki FCP, a także mniejsze wartości TBT i INP, o ile ograniczysz ilość kodu JavaScript po stronie klienta na swoich stronach. W przeciwieństwie do renderowania po stronie serwera zapewnia ono też konsekwentnie szybki czas TTFB, ponieważ kod HTML strony nie musi być generowany dynamicznie na serwerze. Zazwyczaj renderowanie statyczne polega na wygenerowaniu osobnego pliku HTML dla każdego adresu URL z wyprzedzeniem. Dzięki odpowiedziom HTML generowanym z wyprzedzeniem możesz wdrażać statyczne rendery w wielu sieciach CDN, aby korzystać z buforowania brzegowego.

Diagram przedstawiający statyczne renderowanie i opcjonalne wykonywanie kodu JS, które wpływa na FCP i TTI
FCP i TTI z renderowaniem statycznym.

Rozwiązania do renderowania statycznych obrazów występują w różnych kształtach i rozmiarach. Narzędzia takie jak Gatsby są zaprojektowane tak, aby deweloperzy mieli wrażenie, że ich aplikacja jest renderowana dynamicznie, a nie generowana jako część procesu kompilacji. Narzędzia do generowania witryn statycznych, takie jak 11ty, Jekyll i Metalsmith, wykorzystują statyczny charakter tych witryn, zapewniając bardziej elastyczne podejście oparte na szablonach.

Jedną z wad renderowania statycznego jest to, że musi ono generować osobne pliki HTML dla każdego możliwego adresu URL. Może to być trudne lub nawet niemożliwe, gdy nie możesz przewidzieć, jakie będą te adresy URL, lub w przypadku witryn z dużą liczbą unikalnych stron.

Użytkownicy Reacta mogą znać Gatsby'ego, Next.js static export lub Navi, które ułatwiają tworzenie stron z komponentów. Jednak renderowanie statyczne i wstępny proces renderowania działają inaczej: strony renderowane statycznie są interaktywne bez konieczności wykonywania wielu poleceń JavaScript po stronie klienta, podczas gdy wstępny proces renderowania poprawia FCP aplikacji jednostronicowej, która musi być uruchamiana po stronie klienta, aby strony były naprawdę interaktywne.

Jeśli nie masz pewności, czy dane rozwiązanie to renderowanie statyczne czy wstępna, wyłącz JavaScript i wczytaj stronę, którą chcesz przetestować. W przypadku stron renderowanych statycznie większość elementów interaktywnych nadal działa bez JavaScriptu. Strony z zarenderowanymi elementami mogą nadal zawierać niektóre podstawowe funkcje, takie jak linki z wyłączonym JavaScriptem, ale większość strony jest nieaktywna.

Innym przydatnym testem jest ograniczenie przepustowości sieci w Narzędziach deweloperskich w Chrome. Dzięki niemu możesz sprawdzić, ile kodu JavaScript pobiera się przed tym, jak strona stanie się interaktywna. Aby uczynić stronę interaktywną, należy zazwyczaj stosować więcej kodu JavaScriptu, który jest zwykle bardziej złożony niż w przypadku podejścia progressive enhancement używanego w renderowaniu statycznym.

Renderowanie po stronie serwera a renderowanie statyczne

Renderowanie po stronie serwera nie jest najlepszym rozwiązaniem w każdym przypadku, ponieważ jego dynamiczna natura może generować znaczne koszty przetwarzania. Wiele rozwiązań do renderowania po stronie serwera nie powoduje wczesnego wymuszania, opóźnień TTFB ani podwójnego wysyłania danych (np. wbudowanych stanów używanych przez JavaScript po stronie klienta). W React funkcja renderToString() może działać wolno, ponieważ jest synchroniczna i jednowątkowa. Nowsze interfejsy API DOM serwera React obsługują strumieniowanie, dzięki któremu początkowa część odpowiedzi HTML może zostać przesłana do przeglądarki wcześniej, podczas gdy reszta jest nadal generowana na serwerze.

Aby renderowanie po stronie serwera było prawidłowe, może być konieczne znalezienie lub stworzenie rozwiązania do buforowania komponentów, zarządzania zużyciem pamięci, korzystania z technik memoryzacji i rozwiązania innych problemów. Często przetwarzasz lub odtwarzasz tę samą aplikację dwukrotnie – raz po stronie klienta, a raz na serwerze. Renderowanie po stronie serwera, które powoduje szybsze wyświetlanie treści, niekoniecznie oznacza, że będziesz mieć mniej pracy. Jeśli po otrzymaniu przez klienta odpowiedzi HTML wygenerowanej przez serwer masz dużo pracy na kliencie, może to nadal powodować dłuższy czas wczytywania treści i czas ładowania strony.

Renderowanie po stronie serwera generuje kod HTML na żądanie dla każdego adresu URL, ale może być wolniejsze niż wyświetlanie statycznych treści. Jeśli możesz poświęcić na to dodatkowy czas, renderowanie po stronie serwera i buforowanie kodu HTML mogą znacznie skrócić czas renderowania po stronie serwera. Zaletą renderowania po stronie serwera jest możliwość pobierania większej ilości „żywych” danych i odpowiedzi na pełniejszy zestaw żądań niż w przypadku renderowania statycznego. Strony, które wymagają personalizacji, to konkretny przykład żądania, które nie działa dobrze ze statycznym renderowaniem.

Wyświetlanie po stronie serwera może też wiązać się z podjęciem ciekawych decyzji podczas tworzenia PWA czy lepiej użyć elementu usługi do buforowania całej strony, czy tylko wyświetlać poszczególne elementy treści po stronie serwera?

Renderowanie po stronie klienta

Renderowanie po stronie klienta oznacza renderowanie stron bezpośrednio w przeglądarce za pomocą JavaScriptu. Cała logika, pobieranie danych, szablony i przekierowywanie są obsługiwane na kliencie, a nie na serwerze. W efekcie na urządzenie użytkownika z serwera jest przekazywanych więcej danych, co wiąże się z pewnymi kompromisami.

Renderowanie po stronie klienta może być trudne do wykonania i utrzymania na urządzeniach mobilnych. Dzięki nieznacznemu wysiłkowi, aby zachować ścisły budżet JavaScriptu i osiągnąć wartość w jak najmniejszej liczbie wywołań, możesz uzyskać renderowanie po stronie klienta, które prawie w pełni odzwierciedla wydajność renderowania po stronie serwera. Aby przyspieszyć działanie parsowania, możesz przesłać kluczowe skrypty i dane za pomocą <link rel=preload>. Zalecamy też używanie wzorów takich jak PRPL, aby zapewnić błyskawiczne przełączanie się między początkową a kolejną nawigacją.

Diagram pokazujący renderowanie po stronie klienta wpływające na FCP i TTI
FCP i TTI z renderowaniem po stronie klienta.

Główną wadą renderowania po stronie klienta jest to, że wraz ze wzrostem aplikacji wzrasta wymagana ilość kodu JavaScript, co może wpływać na INP strony. Jest to szczególnie trudne w przypadku nowych bibliotek JavaScript, polyfillów i kodu zewnętrznego, które konkurują o moc obliczeniową i często muszą zostać przetworzone, zanim treści strony zostaną wyrenderowane.

W przypadku aplikacji, które korzystają z renderowania po stronie klienta i zawierają duże pakiety kodu JavaScript, warto zastosować agresywne dzielenie kodu, aby zmniejszyć TBT i INP podczas wczytywania strony, a także opóźnione wczytywanie kodu JavaScript, aby dostarczać użytkownikom tylko to, czego potrzebują, i wtedy, kiedy tego potrzebują. W przypadku aplikacji o małej lub zerowej interaktywności renderowanie po stronie serwera może być bardziej skalowalne rozwiązaniem tych problemów.

Jeśli tworzysz aplikacje jednostronicowe, możesz zidentyfikować główne części interfejsu użytkownika, które są wspólne dla większości stron, i zastosować technikę buforowania powłoki aplikacji. W połączeniu ze skryptami service worker może to znacznie poprawić postrzeganą wydajność podczas powtarzanych wizyt, ponieważ strona może bardzo szybko wczytać swoją osłonę aplikacji HTML i zależne zasoby z CacheStorage.

Rehydration łączy renderowanie po stronie serwera i po stronie klienta

Rehydration to podejście, które ma na celu zniwelowanie kompromisów między renderowaniem po stronie klienta a renderowaniem po stronie serwera przez zastosowanie obu tych metod. Żądania dotyczące nawigacji, takie jak wczytywanie lub ponowne wczytywanie całej strony, są obsługiwane przez serwer, który renderuje aplikację do kodu HTML. Następnie kod JavaScript i dane używane do renderowania są umieszczane w wygenerowanym dokumencie. Jeśli zrobisz to ostrożnie, uzyskasz szybkie FCP, czyli renderowanie po stronie klienta, które „przejmuje” renderowanie po stronie serwera. Jest to skuteczne rozwiązanie, ale może mieć znaczne wady pod względem wydajności.

Główną wadą renderowania po stronie serwera z rehydratyzacją jest to, że może ono mieć znaczący negatywny wpływ na TBT i INP, nawet jeśli poprawia FCP. Strony renderowane po stronie serwera mogą wyglądać na załadowane i interaktywne, ale nie będą reagować na dane wejściowe, dopóki nie zostaną wykonane skrypty po stronie klienta dla komponentów i nie zostaną dołączone moduły obsługi zdarzeń. Na urządzeniu mobilnym może to zająć kilka minut, co może dezorientować i frustrować użytkownika.

Problem z rehydratacją: jedna aplikacja w cenie dwóch

Aby kod JavaScript po stronie klienta mógł dokładnie „wziąć pod uwagę” to, co zostało już wykonane na serwerze, bez ponownego wysyłania wszystkich danych, za pomocą których serwer wyrenderował kod HTML, większość rozwiązań do renderowania po stronie serwera serializuje odpowiedź z zależnościami danych interfejsu użytkownika w postaci tagów skryptu w dokumencie. Ponieważ duplikuje to wiele elementów HTML, rehydration może spowodować więcej problemów niż tylko opóźnienie interakcji.

dokument HTML zawierający serializowany interfejs użytkownika, wbudowane dane i skrypt bundle.js;
Duplikat kodu w dokumencie HTML.

Serwer zwraca opis interfejsu aplikacji w odpowiedzi na żądanie nawigacyjne, ale zwraca też dane źródłowe użyte do skompilowania tego interfejsu oraz pełną kopię implementacji interfejsu, która następnie uruchamia się na kliencie. Interaktywność interfejsu bundle.js jest dostępna dopiero po zakończeniu wczytywania i wykonywania.

Dane o wydajności zebrane z prawdziwych witryn, które korzystają z renderowania po stronie serwera i rehydratacji, wskazują, że jest to rzadko najlepsza opcja. Najważniejszym powodem jest wpływ na wrażenia użytkownika, gdy strona wygląda na gotową, ale żadna z jej funkcji interaktywnych nie działa.

Diagram pokazujący, jak renderowanie po stronie klienta negatywnie wpływa na TTI
Wpływ renderowania po stronie klienta na TTI.

Istnieje jednak nadzieja na renderowanie po stronie serwera z rehydratacją. W krótkim terminie korzystanie z renderowania po stronie serwera w przypadku treści, które można łatwo zarchiwizować, może zmniejszyć wartość TTFB, co da podobne wyniki jak wstępna walidacja. Odwodnienie stopniowe, progresywne lub częściowe może być kluczem do zwiększenia przydatności tej techniki w przyszłości.

strumieniowego renderowania po stronie serwera i rehydratacji w miarę postępu strumieniowania.

W ciągu ostatnich kilku lat renderowanie po stronie serwera przeszło wiele zmian.

Strumieniowe renderowanie po stronie serwera umożliwia wysyłanie kodu HTML w kawałkach, które przeglądarka może renderować w miarę ich otrzymywania. Dzięki temu użytkownicy szybciej otrzymają znaczniki, co przyspieszy proces wdrażania funkcji FCP. W React strumienie asynchroniczne w renderToPipeableStream(), w porównaniu z synchronicznymi renderToString(), oznaczają, że ciśnienie wsteczne jest dobrze obsługiwane.

Warto też rozważyć stopniowe nawodnienie, które zostało wdrożone w React. Dzięki temu poszczególne elementy aplikacji renderowanej na serwerze są „uruchamiane” w czasie, zamiast stosować obecne powszechne podejście polegające na inicjowaniu całej aplikacji naraz. Dzięki temu możesz zmniejszyć ilość kodu JavaScript potrzebną do stworzenia interaktywnych stron, ponieważ możesz odroczyć uaktualnianie po stronie klienta elementów o niskim priorytecie, aby nie blokowały one wątku głównego. W ten sposób interakcje użytkownika będą się odbywać szybciej po ich zainicjowaniu.

Progresywne odtwarzanie może też pomóc w uniknięciu jednego z najczęstszych błędów związanych z odtwarzaniem po stronie serwera: drzewo DOM renderowane po stronie serwera jest usuwane, a następnie odtwarzane od nowa, najczęściej dlatego, że początkowe synchroniczne renderowanie po stronie klienta wymagało danych, które nie były jeszcze gotowe, często Promise, które nie zostało jeszcze rozwiązane.

częściowe nawodnienie,

Częściowe nawodnienie okazało się trudne do wdrożenia. Jest to rozszerzenie progresywnego rehydratacji, które analizuje poszczególne elementy strony (składniki, widoki lub drzewa) i identyfikuje elementy o małej interakcji lub bez reakcji. W przypadku każdego z tych elementów, które są w większości statyczne, odpowiedni kod JavaScript jest następnie przekształcany w nieaktywne odwołania i funkcje dekoracyjne, co powoduje, że ich wpływ na klienta jest minimalny.

Częściowe nawilżanie ma swoje problemy i wymaga kompromisów. Wyzwanie to ciekawe wyzwania dla pamięci podręcznej, a nawigacja po stronie klienta oznacza, że nie możemy zakładać, że kod HTML renderowany po stronie serwera w przypadku nieaktywnych części aplikacji jest dostępny bez pełnego wczytania strony.

Renderowanie trysomorficznego

Jeśli usługa workera jest dla Ciebie odpowiednia, rozważ renderowanie trisomorficzne. Jest to technika, która pozwala na strumieniowe renderowanie po stronie serwera na potrzeby początkowych nawigacji lub nawigacji bez JavaScriptu, a potem zleca renderowanie kodu HTML na potrzeby nawigacji usługowemu workerowi po jego zainstalowaniu. Dzięki temu komponenty i szablony w pamięci podręcznej będą zawsze aktualne, a przechodzenie między widokami w ramach tej samej sesji będzie możliwe w sposób podobny do aplikacji SPA. To podejście sprawdza się najlepiej, gdy możesz udostępniać ten sam kod szablonów i kod kierowania między serwerem, stroną klienta i usługą.

Renderowanie trójstronne, pokazujące przeglądarkę i usługę workera komunikujące się z serwerem.
Schemat działania renderowania trysomorficznego.

Uwagi dotyczące SEO

Wybierając strategię renderowania stron internetowych, zespoły często biorą pod uwagę wpływ SEO. Renderowanie po stronie serwera to popularny sposób na zapewnienie „pełnego” wyglądu, który może interpretować robot. Roboty mogą interpretować kod JavaScript, ale często występują ograniczenia dotyczące sposobu ich renderowania. Renderowanie po stronie klienta może działać, ale często wymaga dodatkowego testowania i zasobów. Ostatnio renderowanie dynamiczne stało się również opcją wartą rozważenia, jeśli Twoja architektura jest silnie zależna od kodu JavaScript po stronie klienta.

W razie wątpliwości narzędzia do testowania optymalizacji mobilnej to świetny sposób na sprawdzenie, czy wybrane podejście spełnia Twoje oczekiwania. Pokazuje wizualny podgląd tego, jak strona wygląda dla robota Google, serializowany kod HTML znaleziony po wykonaniu kodu JavaScript oraz wszelkie błędy napotkane podczas renderowania.

Interfejs testowania optymalizacji mobilnej
Interfejs testu optymalizacji mobilnej.

Podsumowanie

Wybierając metodę renderowania, zmierz i zrozumieć, jakie są Twoje wąskie gardła. Zastanów się, czy renderowanie statyczne lub renderowanie po stronie serwera może w większości spełnić Twoje oczekiwania. Aby uzyskać interaktywność, możesz przesyłać głównie kod HTML z minimalną ilością kodu JavaScript. Oto przydatna infografika przedstawiająca spektrum serwer-klient:

Infografika przedstawiająca spektrum opcji opisanych w tym artykule
Opcje renderowania i ich wady i zalety.

Środki

Dziękujemy wszystkim za opinie i inspirację:

Jeffrey Posnick, Houssein Djirdeh, Shubhie Panicker, Chris Harrelson, Sebastian Markbåge