Renderowanie w internecie

Jedną z głównych decyzji, które muszą podjąć deweloperzy stron internetowych, jest to, gdzie w aplikacji wdrożyć logikę i renderowanie. Może to być trudne, ponieważ istnieje wiele sposobów tworzenia witryny.

Wiedza o tym obszarze jest uzależniona od wyników naszych działań w Chrome, prowadzonych w ciągu ostatnich kilku lat w rozmowach z dużymi witrynami. Ogólnie rzecz biorąc, zachęcamy programistów, aby rozważali renderowanie po stronie serwera lub renderowanie statyczne, zamiast stosowania tej metody w pełni nawadnianej.

Aby lepiej zrozumieć poszczególne architektury, które wybieramy podczas podejmowania decyzji, potrzebujemy dogłębnego zrozumienia każdego podejścia i spójnej terminologii, którą należy stosować podczas omawiania tych kwestii. Różnice między metodami renderowania pomagają zilustrować kompromisy związane z renderowaniem w internecie z perspektywy wydajności stron.

Terminologia

renderowanie,

Renderowanie po stronie serwera (SSR)
Renderowanie aplikacji po stronie klienta lub aplikacji uniwersalnej na kod HTML na serwerze.
Renderowanie po stronie klienta
Renderowanie aplikacji w przeglądarce z wykorzystaniem JavaScriptu do modyfikacji DOM.
Nawodnienie
Uruchamianie widoków JavaScriptu na kliencie, dzięki czemu wykorzystuje renderowane przez serwer drzewo DOM i dane HTML HTML.
Renderowanie wstępne
uruchamianie aplikacji po stronie klienta podczas kompilacji w celu uchwycenia jej początkowego stanu w postaci statycznego kodu HTML;

Wydajność

Czas do pierwszego bajtu (TTFB)
Czas między kliknięciem linku a pierwszym bajtem treści wczytywanym na nowej stronie.
Pierwsze wyrenderowanie treści (FCP)
Czas, w którym żądana treść (treść artykułu itp.) staje się widoczna.
Interakcja do kolejnego wyrenderowania (INP)
Reprezentatywna wartość, która określa, czy strona stale reaguje na dane wejściowe użytkownika.
Całkowity czas blokowania (TBT)
Dodatkowe dane dla INP, które obliczają, jak długo wątek główny 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ę. Pozwala to uniknąć dodatkowych transferów danych i szablonów w obie strony po stronie klienta, ponieważ mechanizm renderowania obsługuje je, zanim przeglądarka otrzyma odpowiedź.

Renderowanie po stronie serwera zwykle zapewnia szybkie FCP. Uruchomienie logiki strony i renderowanie stron na serwerze pozwala uniknąć wysyłania dużej ilości kodu JavaScript do klienta. Pomaga to zmniejszyć wartość TBT strony, która może również prowadzić do niższego INP, ponieważ wątek główny nie jest blokowany tak często podczas jej wczytywania. Gdy wątek główny jest rzadziej blokowany, interakcje użytkownika mają więcej okazji do szybszego uruchomienia. Ma to sens, bo w przypadku renderowania po stronie serwera wysyłasz tekst i linki do przeglądarki użytkownika. Ta metoda sprawdza się w przypadku różnych urządzeń i sieci oraz otwiera interesujące optymalizacje w przeglądarce, np. analizowanie strumieniowe dokumentów.

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

Dzięki renderowaniu po stronie serwera jest mniej prawdopodobne, że użytkownicy będą musieli czekać na uruchomienie kodu JavaScript powiązanego z procesorem, zanim będą mogli skorzystać z Twojej witryny. Nawet jeśli nie możesz uniknąć stosowania kodu JavaScript innej firmy, skorzystanie z renderowania po stronie serwera w celu ograniczenia własnych kosztów JavaScriptu może zwiększyć budżet na resztę. Jednak z takim podejściem może się wiązać jeden z problemów: generowanie stron na serwerze wymaga czasu, a to może przełożyć się na większą liczbę TTFB strony.

To, czy renderowanie po stronie serwera sprawdzi się w Twojej aplikacji, zależy w dużej mierze od rodzaju tworzonego środowiska. Toczy się od dawna dyskusja na temat tego, czy renderowanie po stronie serwera jest właściwe, a jakie powinno być po stronie klienta, ale zawsze można zdecydować się na zastosowanie renderowania po stronie serwera w przypadku niektórych stron, a nie innych. W niektórych witrynach z powodzeniem zastosowano techniki renderowania hybrydowego. Serwer Netflixrenderuje stosunkowo statyczne strony docelowe na serwerze, a pobiera z wyprzedzeniemkod JavaScript do stron intensywnie wymagających interakcji, co zwiększa szanse na szybkie wczytywanie takich stron.

Wiele nowoczesnych platform, bibliotek i architektur umożliwia renderowanie tej samej aplikacji zarówno po stronie klienta, jak i na serwerze. Możesz ich używać do renderowania po stronie serwera. Jednak architektury, w których renderowanie odbywa się zarówno na serwerze, jak i po stronie klienta, są odrębną klasą rozwiązań o bardzo różniących się parametrach wydajności i związanych z tym kompromisami. Użytkownicy reakcji mogą używać interfejsów API DOM serwera lub rozwiązań na ich podstawie, np. Next.js, aby renderować po stronie serwera. Użytkownicy Vue mogą skorzystać z przewodnika po renderowaniu po stronie serwera Vue lub Nuxt. Angular oferuje Universal. Większość popularnych rozwiązań stosuje jednak pewne formy nawodnienia, więc zapoznaj się ze sposobami, których używa Twoje narzędzie.

Renderowanie statyczne

Renderowanie statyczne ma miejsce w czasie kompilacji. To podejście zapewnia szybki FCP i niższą wartość TBT i INP, o ile ograniczysz ilość pliku JS po stronie klienta na Twoich stronach. W przeciwieństwie do renderowania po stronie serwera pliki TTFB cechują się niezmienną szybkością, ponieważ kod HTML strony nie musi być generowany dynamicznie na serwerze. Ogólnie rzecz biorąc, renderowanie statyczne polega na utworzeniu z wyprzedzeniem osobnego pliku HTML dla każdego adresu URL. Po wygenerowaniu z wyprzedzeniem odpowiedzi HTML możesz wdrożyć renderowanie statyczne w wielu sieciach CDN, aby skorzystać z buforowania brzegowego.

Diagram przedstawiający renderowanie statyczne i opcjonalne wykonanie kodu JS wpływającego na FCP i TI.
FCP i TI z renderowaniem statycznym.

Rozwiązania do renderowania statycznego mają różne kształty i rozmiary. Dzięki narzędziom takim jak Gatsby deweloperzy mają wrażenie, że ich aplikacja jest renderowana dynamicznie, a nie generowana jako etap kompilacji. Statyczne narzędzia do generowania witryn, takie jak 11ty, Jekyll i Metalsmith, radzą sobie ze statyczną naturą, umożliwiając w ten sposób podejście oparte na szablonach.

Jedną z zalet renderowania statycznego jest to, że musi ono generować osobne pliki HTML dla każdego możliwego adresu URL. Może to być trudne, a nawet niewykonalne, gdy nie można z wyprzedzeniem przewidzieć, jakie adresy URL będą się pojawiać, lub w przypadku witryn z dużą liczbą niepowtarzalnych stron.

Użytkownicy React mogą znać usługi Gatsby, eksportu statycznego Next.js lub Navi, które ułatwiają tworzenie stron na podstawie komponentów. Renderowanie statyczne i renderowanie wstępne działają jednak inaczej: strony renderowane statycznie są interaktywne i nie wymagają wykonywania dużej ilości kodu JavaScript po stronie klienta. Natomiast renderowanie wstępne poprawia FCP aplikacji jednostronicowej, którą trzeba uruchomić po stronie klienta, aby strony były naprawdę interaktywne.

Jeśli nie masz pewności, czy dane rozwiązanie to renderowanie statyczne czy renderowanie wstępne, wyłącz JavaScript i załaduj stronę, którą chcesz przetestować. Na stronach renderowanych statycznie większość funkcji interaktywnych nadal występuje bez JavaScriptu. Wstępnie renderowane strony mogą nadal mieć niektóre podstawowe funkcje, takie jak linki z wyłączonym JavaScriptem, ale większość stron jest bezwładna.

Innym przydatnym testem jest użycie ograniczania sieci w Narzędziach deweloperskich w Chrome i sprawdzenie, ile JavaScriptu pobiera się, zanim strona stanie się interaktywna. Renderowanie wstępne zwykle wymaga większej ilości kodu JavaScript, aby stał się interaktywny, a JavaScript jest zwykle bardziej złożony niż metoda udoskonalania progresywnego używana 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 dynamiczny charakter może wiązać się z znacznymi kosztami mocy obliczeniowej. Wiele rozwiązań do renderowania po stronie serwera nie jest opróżniane wcześniej, opóźnia technologię TTFB ani podwaja ilość przesyłanych danych (np. stany w tekście używane przez JavaScript po stronie klienta). Funkcja renderToString() może działać wolno w reakcji, ponieważ jest synchroniczna i jednowątkowa. Nowsze interfejsy DOM API serwera React obsługują strumieniowanie, dzięki czemu początkowa część odpowiedzi HTML jest wysyłana do przeglądarki szybciej, gdy reszta jest generowana na serwerze.

Uzyskanie właściwego renderowania po stronie serwera może wymagać znalezienia lub stworzenia rozwiązania do buforowania komponentów, zarządzania wykorzystaniem pamięci, korzystania z technik zapamiętywania i innych problemów. Często przetwarzasz lub odbudowujesz tę samą aplikację dwa razy – na kliencie i na serwerze. Renderowanie po stronie serwera, które pozwoli wyświetlić treści wcześniej, nie musi oznaczać mniej pracy. Jeśli po otrzymaniu od klienta odpowiedzi HTML wygenerowanej przez serwer musisz wykonać dużo pracy, i tak możesz zwiększyć wartość TBT i INP w przypadku Twojej witryny.

Renderowanie po stronie serwera tworzy na żądanie kod HTML dla każdego adresu URL, ale może być wolniejsze niż wyświetlanie statycznych treści renderowanych. Jeśli możesz wykonać dodatkowe czynności, renderowanie po stronie serwera i buforowanie kodu HTML mogą znacznie skrócić czas renderowania serwera. Wadą renderowania po stronie serwera jest możliwość pobrania większej ilości „aktywnych” danych i reagowania na pełniejszy zestaw żądań niż w przypadku renderowania statycznego. Konkretnym przykładem żądań, które nie sprawdzają się w przypadku renderowania statycznego, są strony, które wymagają personalizacji.

Renderowanie po stronie serwera może też wiązać się z interesującymi decyzjami podczas tworzenia aplikacji PWA: czy lepiej korzystać z pamięci podręcznej w postaci skryptu service worker zajmującej całą stronę, czy tylko z renderować poszczególne fragmenty treści na serwer.

Renderowanie po stronie klienta

Renderowanie po stronie klienta oznacza renderowanie stron bezpośrednio w przeglądarce za pomocą języka JavaScript. Wszystkie funkcje logiczne, pobieranie danych, tworzenie szablonów i routing są obsługiwane po stronie klienta, a nie na serwerze. Efektem jest to, że serwer przekazuje do urządzenia użytkownika więcej danych, co wiąże się z własnym zestawem kompromisów.

Renderowanie po stronie klienta może być trudne do wykonania i utrzymać szybkość na urządzeniach mobilnych. Wystarczy trochę pracy, by utrzymać ograniczony budżet JavaScript i uzyskiwać wartość w ramach jak najmniejszej liczby przebiegów w obie strony, aby uzyskać renderowanie po stronie klienta niemal naśladującym wydajność renderowania po stronie serwera. Możesz przyspieszyć pracę parsera, jeśli dostarczysz kluczowe skrypty i dane za pomocą <link rel=preload> Zalecamy też użycie wzorców takich jak PRPL, aby zapewnić błyskawiczną nawigację podczas pierwszej i kolejnej nawigacji.

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

Główną wadą renderowania po stronie klienta jest to, że ilość wymaganego kodu JavaScript rośnie wraz ze wzrostem aplikacji, co może wpływać 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 przed wyrenderowaniem zawartości strony.

W środowiskach, które korzystają z renderowania po stronie klienta i korzystają z dużych pakietów JavaScript, warto rozważyć agresywne podział kodu w celu zmniejszenia wartości TBT i INP podczas wczytywania strony oraz leniwe ładowanie kodu JavaScript, aby wyświetlać tylko to, czego potrzebuje użytkownik, gdy jest to konieczne. W przypadku rozwiązań o małej interaktywności lub braku interaktywności renderowanie po stronie serwera może być bardziej skalowalnym rozwiązaniem tych problemów.

W przypadku osób tworzących aplikacje jednostronicowe określenie głównych części interfejsu użytkownika udostępnianych przez większość stron umożliwia zastosowanie metody buforowania powłoki aplikacji. W połączeniu z mechanizmami Service Worker może znacznie poprawić wydajność przy ponownych wizytach, ponieważ strona może bardzo szybko wczytywać kod HTML powłoki aplikacji i zależności z CacheStorage.

Rehydratacja łączy renderowanie po stronie serwera i klienta

Ponowne nawodnienie to metoda, która próbuje wygładzić kompromis między renderowaniem po stronie klienta i po stronie serwera przez wykonanie obu tych czynności. Żądania nawigacyjne, np. pełne wczytanie strony lub przeładowania, są obsługiwane przez serwer, który renderuje aplikację w formacie HTML, a potem kod JavaScript i dane używane do renderowania są osadzone w wynikowym dokumencie. Dobrze przygotowane rozwiązanie zapewnia szybkie FCP, takie jak renderowanie po stronie serwera, a następnie „nabiera się” przez ponowne renderowanie na kliencie. Jest to skuteczne rozwiązanie, ale ma poważne wady.

Główną 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 poprawi FCP. Strony renderowane po stronie serwera mogą wydawać się wczytane i interaktywne, ale nie mogą reagować na dane wejściowe, dopóki po stronie klienta nie zostaną wykonane skrypty komponentów i dołączone moduły obsługi zdarzeń. Na urządzeniach mobilnych może to chwilę dezorientować i frustrować użytkownika.

Problem z nawodnieniem: jedna aplikacja w cenie dwóch

Aby kod JavaScript po stronie klienta mógł dokładnie „rozpocząć” go w miejscu, w którym serwer został przerwany, bez ponownego żądania wszystkich danych, z którymi serwer wyrenderował kod HTML, większość rozwiązań do renderowania po stronie serwera zserializowała odpowiedź z zależności danych interfejsu użytkownika jako tagi skryptu w dokumencie. Ponieważ powiela to dużą część kodu HTML, rehydratacja może spowodować więcej problemów niż tylko opóźnioną interaktywność.

Dokument HTML zawierający zserializowany interfejs użytkownika, wbudowane dane i skrypt pakiet.js
Zduplikowany kod w dokumencie HTML.

W odpowiedzi na żądanie nawigacyjne serwer 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ę przez klienta. Interfejs użytkownika nie staje się interaktywny, dopóki nie zakończy się wczytywanie i wykonywanie interfejsu przez bundle.js.

Dane o wydajności zebrane z prawdziwych witryn z użyciem renderowania po stronie serwera i nawodnienia wskazują, że rzadko jest to najlepsza opcja. Najważniejszym powodem jest jego wpływ na wrażenia użytkownika, gdy strona wygląda na gotową, ale nie działają jej funkcje interaktywne.

Diagram pokazujący negatywny wpływ renderowania przez klienta na TTI.
Wpływ renderowania po stronie klienta na TTI.

Jest jednak szansa na renderowanie po stronie serwera z nawadnianiem. W najbliższej perspektywie użycie renderowania po stronie serwera w przypadku treści, które można buforować, może ograniczyć ilość czasu TTFB i uzyskać wyniki podobne do renderowania wstępnego. Nawadnianie stopniowo, stopniowo lub częściowo może być kluczem do usprawnienia tej techniki w przyszłości.

Strumieniuj renderowanie po stronie serwera i stopniowo nawodnić

W ciągu ostatnich kilku lat wprowadziliśmy szereg zmian w renderowaniu po stronie serwera.

Strumieniowe renderowanie po stronie serwera umożliwia wysyłanie kodu HTML we fragmentach, które przeglądarka może renderować stopniowo w miarę odbierania. Dzięki temu możesz szybciej udostępnić znaczniki użytkownikom, co przyspieszy Twój FCP. W reakcji asynchroniczne strumienie w renderToPipeableStream(), w porównaniu ze strumieniami synchronicznymi renderToString(), oznaczają, że wsteczne działanie jest dobrze obsługiwane.

Warto też rozważyć progresywne nawodnienie, które zostało wdrożone w React. W tym przypadku poszczególne elementy aplikacji renderowanej przez serwer są „uruchamiane” wraz z upływem czasu, a nie w przypadku stosowanego obecnie obecnie typowego podejścia do inicjowania całej aplikacji od razu. Pomaga to ograniczyć ilość kodu JavaScript potrzebnego do zapewnienia interaktywności stron, ponieważ umożliwia odroczenie uaktualnienia tych części strony o niskim priorytecie po stronie klienta, tak aby nie blokowały one wątku głównego, a interakcje użytkownika mogą następować wcześniej po ich zainicjowaniu przez użytkownika.

Progresywne nawadnianie może również pomóc uniknąć jednej z najczęstszych problemów z nawadnianiem po stronie serwera: drzewo DOM renderowane przez serwer zostaje zniszczone, a następnie natychmiast odbudowane, najczęściej dlatego, że początkowe synchroniczne renderowanie po stronie klienta wymaga danych, które nie są jeszcze gotowe, czyli Promise, które jeszcze nie zostały rozwiązane.

Częściowe nawodnienie

Częściowe nawodnienie okazało się trudne w implementacji. To podejście jest rozwinięciem progresywnego nawodnienia, które analizuje poszczególne elementy strony (komponenty, wyświetlenia lub drzewa) i identyfikuje elementy o niewielkiej interakcji lub braku reaktywności. W przypadku każdej z tych głównie statycznych części odpowiedni kod JavaScript jest następnie przekształcany w odwołania i funkcje dekoracyjne, co zmniejsza ich ślad po stronie klienta do niemal zera.

Podejście polegające na częściowym nawodnieniu wiąże się z pewnymi problemami i kompromisami. Stanowi to interesujące wyzwania w zakresie buforowania, a nawigacja po stronie klienta oznacza, że nie możemy zakładać, że renderowany serwerowo kod HTML dla bezwładnych części aplikacji jest dostępny bez pełnego wczytania strony.

Renderowanie trisomorficzne

Jeśli możesz zastosować skrypty service worker, rozważ renderowanie trisomorficzne. To technika, która pozwala wykorzystać strumieniowanie po stronie serwera do nawigacji początkowej lub niezawierającej elementów JavaScript, a następnie na wdrożenie przez mechanizm service worker renderowania kodu HTML w elementach nawigacyjnych po jego zainstalowaniu. Dzięki temu komponenty i szablony zapisane w pamięci podręcznej będą aktualne oraz umożliwiają nawigację w stylu SPA do renderowania nowych widoków w tej samej sesji. To podejście sprawdza się najlepiej, gdy można współużytkować ten sam szablon i kod routingu między serwerem, stroną klienta i skryptem service worker.

Diagram renderowania trisomorficznego, w którym przeglądarka i skrypt service worker komunikują się z serwerem.
Schemat działania renderowania trizomorficznego.

Uwagi na temat SEO

Wybierając strategię renderowania stron internetowych, zespoły często biorą pod uwagę wpływ SEO. Renderowanie po stronie serwera jest popularnym rozwiązaniem, jeśli zależy Ci na zapewnieniu „pełnego wyglądu”, które roboty mogą interpretować. Roboty rozumieją JavaScript, ale często obowiązują ograniczenia dotyczące sposobu renderowania. Renderowanie po stronie klienta może działać, ale często wymaga dodatkowych testów i nakładów pracy. Ostatnio warto rozważyć renderowanie dynamiczne, jeśli architektura w dużym stopniu bazuje na języku JavaScript po stronie klienta.

W razie wątpliwości narzędzie do testowania optymalizacji mobilnej to świetny sposób, by sprawdzić, czy wybrane podejście odpowiada Twoim oczekiwaniom. Zawiera wizualny podgląd dowolnej strony w przeglądarce dla robota Google, zserializowaną zawartość HTML znalezionych po wykonaniu JavaScriptu oraz wszelkie błędy napotkane podczas renderowania.

Zrzut ekranu interfejsu testu optymalizacji mobilnej.
Interfejs użytkownika do testowania optymalizacji mobilnej.

Podsumowanie

Podejmując decyzję o podejściu do renderowania, zmierz i poznaj Twoje słabe gardła. Zastanów się, czy pomoże Ci to najlepiej: renderowanie statyczne lub renderowanie po stronie serwera. Najlepiej jest przesyłać kod HTML z użyciem minimalnego JavaScriptu, aby zapewnić interaktywność. Oto przydatna infografika zawierająca spektrum serwer-klient:

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

Środki

Dziękujemy wszystkim za opinie i inspirację:

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