Renderowanie w internecie

Data publikacji: 6 lutego 2019 r., ostatnia aktualizacja: 5 stycznia 2026 r.

Jedną z kluczowych decyzji, jakie muszą podjąć programiści stron internetowych, jest to, gdzie w aplikacji wdrożyć logikę i renderowanie. Może to być trudne, ponieważ istnieje wiele sposobów tworzenia witryn.

Nasze rozumienie tej przestrzeni opiera się na naszej pracy w Chrome i rozmowach z dużymi witrynami w ciągu ostatnich kilku lat. Ogólnie rzecz biorąc, zachęcamy programistów do rozważenia renderowania po stronie serwera lub renderowania statycznego zamiast pełnej hydratacji.

Aby lepiej zrozumieć architektury, z których korzystamy przy podejmowaniu tej decyzji, potrzebujemy spójnej terminologii i wspólnych ram dla każdego podejścia. Dzięki temu możesz lepiej ocenić kompromisy związane z każdym podejściem do renderowania z perspektywy wydajności strony.

Terminologia

Najpierw zdefiniujemy kilka terminów, których będziemy używać.

Renderowanie

Renderowanie po stronie serwera (SSR)
Renderowanie aplikacji na serwerze w celu wysyłania do klienta kodu HTML zamiast JavaScriptu.
Renderowanie po stronie klienta (CSR)
Renderowanie aplikacji w przeglądarce za pomocą JavaScriptu do modyfikowania DOM.
Renderowanie wstępne
Uruchamianie aplikacji po stronie klienta w czasie kompilacji, aby przechwycić jej stan początkowy jako statyczny kod HTML. Uwaga: „wstępne renderowanie” w tym kontekście różni się od wstępnego renderowania przyszłych nawigacji przez przeglądarkę.
Nawodnienie
Uruchamianie skryptów po stronie klienta w celu dodania stanu aplikacji i interaktywności do kodu HTML renderowanego po stronie serwera. Hydracja zakłada, że DOM się nie zmienia.
Ożywianie
Chociaż często jest używane w tym samym znaczeniu co hydratacja, ożywianie oznacza regularne aktualizowanie DOM najnowszym stanem, w tym po początkowej hydratacji.

Wydajność

Czas do pierwszego bajtu (TTFB)
Czas między kliknięciem linku a załadowaniem pierwszego bajtu treści na nowej stronie.
Pierwsze wyrenderowanie treści (FCP)
Czas, w którym żądane treści (treść artykułu itp.) stają się widoczne.
Interakcja do kolejnego wyrenderowania (INP)
Reprezentatywne dane, które oceniają, czy strona szybko reaguje na dane wejściowe użytkownika.
Łączny czas blokowania (TBT)
Dane zastępcze 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ę. Pozwala to uniknąć dodatkowych podróży w obie strony w celu pobrania danych i utworzenia szablonu po stronie klienta, ponieważ moduł renderujący obsługuje je, zanim przeglądarka otrzyma odpowiedź.

Renderowanie po stronie serwera zwykle zapewnia szybki FCP. Uruchamianie logiki strony i renderowanie na serwerze pozwala uniknąć wysyłania dużej ilości JavaScriptu do klienta. Pomaga to zmniejszyć TBT strony, co może również 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 rzadziej blokowany, interakcje użytkownika mają więcej możliwości wcześniejszego uruchomienia.

Ma to sens, ponieważ w przypadku renderowania po stronie serwera do przeglądarki użytkownika wysyłane są tylko tekst i linki. To podejście sprawdza się w różnych warunkach urządzenia i sieci oraz otwiera ciekawe możliwości optymalizacji przeglądarki, takie jak strumieniowe analizowanie dokumentów.

Diagram pokazujący, jak renderowanie po stronie serwera i wykonywanie JavaScriptu wpływają na FCP i TTI.
FCP i TTI z renderowaniem po stronie serwera.

Renderowanie po stronie serwera zmniejsza prawdopodobieństwo, że użytkownicy będą musieli czekać na uruchomienie JavaScriptu, który obciąża procesor, zanim będą mogli korzystać z Twojej witryny. Nawet jeśli nie możesz uniknąć kodu JavaScriptu firmy zewnętrznej, używanie renderowania po stronie serwera w celu zmniejszenia własnych kosztów JavaScriptu może zapewnić Ci większy budżet na pozostałe działania. Ta metoda ma jednak jedną potencjalną wadę: generowanie stron na serwerze zajmuje czas, co może zwiększyć TTFB strony.

To, czy renderowanie po stronie serwera wystarczy w przypadku Twojej aplikacji, zależy w dużej mierze od rodzaju tworzonego środowiska. Od dawna toczy się dyskusja na temat prawidłowego stosowania renderowania po stronie serwera i renderowania po stronie klienta, ale zawsze możesz wybrać renderowanie po stronie serwera w przypadku niektórych stron, a w przypadku innych nie. Niektóre witryny z powodzeniem stosują techniki renderowania hybrydowego. Na przykład Netflix renderuje po stronie serwera stosunkowo statyczne strony docelowe, a jednocześnie prefetching JavaScript do stron wymagających wielu interakcji, co zwiększa szansę na szybkie wczytanie tych bardziej złożonych stron renderowanych po stronie klienta.

W przypadku wielu nowoczesnych platform, bibliotek i architektur możesz renderować tę samą aplikację zarówno na kliencie, jak i na serwerze. Możesz używać tych technik do renderowania po stronie serwera. Architektury, w których renderowanie odbywa się zarówno na serwerze, jak i na urządzeniu klienta, stanowią jednak odrębną klasę rozwiązań o bardzo różnych charakterystykach wydajności i kompromisach. Użytkownicy Reacta mogą używać interfejsów API DOM serwera 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 Nuxt. Angular ma Universal.

Większość popularnych rozwiązań wykorzystuje jakąś formę hydratacji, więc sprawdź, jakie podejście stosuje Twoje narzędzie.

Renderowanie statyczne

Renderowanie statyczne odbywa się w czasie kompilacji. Takie podejście zapewnia szybki FCP, a także niższe 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 też stale szybki TTFB, ponieważ kod HTML strony nie musi być generowany dynamicznie na serwerze. Ogólnie rzecz biorąc, renderowanie statyczne polega na wcześniejszym wygenerowaniu osobnego pliku HTML dla każdego adresu URL. Dzięki wstępnie wygenerowanym odpowiedziom HTML możesz wdrażać statyczne wersje renderowane w wielu sieciach CDN, aby korzystać z tymczasowego przechowywania danych na serwerach brzegowych.

Diagram przedstawiający renderowanie statyczne i opcjonalne wykonywanie JavaScriptu, które wpływają na FCP i TTI.
FCP i TTI w przypadku renderowania statycznego.

Rozwiązania do renderowania statycznego mają różne kształty i rozmiary. 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, takie jak 11ty, Jekyll i Metalsmith, wykorzystują statyczny charakter witryn, zapewniając 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 musisz przewidzieć te adresy URL z wyprzedzeniem, a także w przypadku witryn z dużą liczbą unikalnych stron.

Użytkownicy Reacta mogą znać Gatsby, eksport statyczny Next.js lub Navi, które ułatwiają tworzenie stron z komponentów. Renderowanie statyczne i wstępne działają jednak inaczej: strony renderowane statycznie są interaktywne bez konieczności wykonywania dużej ilości JavaScriptu po stronie klienta, a renderowanie wstępne poprawia FCP aplikacji jednostronicowej, która musi zostać uruchomiona na urządzeniu klienta, aby strony były w pełni interaktywne.

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

Innym przydatnym testem jest użycie ograniczania wykorzystania sieci w Narzędziach deweloperskich w Chrome i sprawdzenie, ile JavaScriptu zostanie pobrane, zanim strona stanie się interaktywna. Wstępne renderowanie zwykle wymaga więcej kodu JavaScript, aby stać się interaktywne, a ten kod JavaScript jest zwykle bardziej złożony niż podejście stopniowego ulepszania stosowane w renderowaniu statycznym.

Renderowanie po stronie serwera a renderowanie statyczne

Renderowanie po stronie serwera nie jest najlepszym rozwiązaniem we wszystkich przypadkach, ponieważ jego dynamiczny charakter może wiązać się ze znacznymi kosztami obliczeniowymi. Wiele rozwiązań do renderowania po stronie serwera nie opróżnia bufora wcześnie, opóźnia TTFB lub podwaja ilość przesyłanych danych (np. stany wbudowane używane 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 czemu początkowa część odpowiedzi HTML może szybciej trafić do przeglądarki, podczas gdy reszta jest nadal generowana na serwerze.

Prawidłowe renderowanie po stronie serwera może wymagać znalezienia lub utworzenia rozwiązania do buforowania komponentów, zarządzania zużyciem pamięci, stosowania technik memoizacji i rozwiązania innych problemów. Często przetwarzasz lub ponownie tworzysz tę samą aplikację 2 razy: raz po stronie klienta i raz po stronie serwera. Renderowanie po stronie serwera, które powoduje szybsze wyświetlanie treści, niekoniecznie oznacza mniej pracy. Jeśli po stronie klienta masz dużo pracy po otrzymaniu przez klienta wygenerowanej przez serwer odpowiedzi HTML, może to nadal prowadzić do wyższych wartości TBT i INP w Twojej witrynie.

Renderowanie po stronie serwera generuje kod HTML na żądanie dla każdego adresu URL, ale może być wolniejsze niż samo wyświetlanie statycznych treści. Jeśli możesz wykonać dodatkową pracę, renderowanie po stronie serwera w połączeniu z pamięcią podręczną HTML może znacznie skrócić czas renderowania po stronie serwera. Zaletą renderowania po stronie serwera jest możliwość pobierania większej ilości „aktualnych” danych i odpowiadania na pełniejszy zestaw żądań niż w przypadku renderowania statycznego. Strony, które wymagają personalizacji, są konkretnym przykładem typu żądania, które nie działa dobrze w przypadku renderowania statycznego.

Renderowanie po stronie serwera może też wiązać się z ciekawymi decyzjami podczas tworzenia PWA. Czy lepiej używać buforowania skryptu service worker na całej stronie czy renderować poszczególne elementy treści na serwerze?

Renderowanie po stronie klienta

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

Renderowanie po stronie klienta może być trudne do szybkiego wykonania i utrzymania na urządzeniach mobilnych. Jeśli poświęcisz trochę czasu na utrzymanie niskiego budżetu na JavaScript i dostarczanie wartości przy jak najmniejszej liczbie przejazdów w obie strony, możesz sprawić, że renderowanie po stronie klienta będzie prawie tak samo wydajne jak renderowanie po stronie serwera. Możesz przyspieszyć działanie parsera, dostarczając krytyczne skrypty i dane za pomocą <link rel=preload>. Zalecamy też stosowanie wzorców takich jak PRPL, aby zapewnić natychmiastowe działanie początkowej i kolejnych nawigacji.

Diagram pokazujący, jak renderowanie po stronie klienta wpływa na FCP i TTI.
FCP i TTI z renderowaniem po stronie klienta.

Główną wadą renderowania po stronie klienta jest to, że wraz z rozwojem aplikacji rośnie ilość wymaganego kodu JavaScript, co może mieć wpływ na INP strony. Staje się to szczególnie trudne, gdy dodawane są nowe biblioteki JavaScriptu, polyfille i kod zewnętrzny, które konkurują o moc obliczeniową i często muszą być przetwarzane, zanim będzie można wyrenderować treść strony.

W przypadku stron, które korzystają z renderowania po stronie klienta i dużych pakietów JavaScriptu, warto rozważyć agresywne dzielenie kodu, aby zmniejszyć TBT i INP podczas wczytywania strony, a także leniwe ładowanie JavaScriptu, aby wyświetlać użytkownikowi tylko to, czego potrzebuje i kiedy tego potrzebuje. W przypadku funkcji o niewielkiej interaktywności lub jej braku renderowanie po stronie serwera może być bardziej skalowalnym rozwiązaniem tych problemów.

Jeśli tworzysz aplikacje jednostronicowe, określenie podstawowych części interfejsu użytkownika, które są wspólne dla większości stron, umożliwia zastosowanie techniki buforowania powłoki aplikacji. W połączeniu z service workerami może to znacznie poprawić postrzeganą wydajność podczas ponownych wizyt, ponieważ strona może bardzo szybko wczytywać HTML powłoki aplikacji i zależności z CacheStorage.

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

Hydracja to podejście, które łagodzi kompromisy między renderowaniem po stronie klienta i po stronie serwera, ponieważ wykorzystuje oba te sposoby. Żądania nawigacji, takie jak pełne wczytanie lub ponowne wczytanie strony, są obsługiwane przez serwer, który renderuje aplikację do formatu HTML. Następnie w wynikowym dokumencie osadzane są JavaScript i dane używane do renderowania. Jeśli zrobisz to ostrożnie, uzyskasz szybki FCP, podobnie jak w przypadku renderowania po stronie serwera, a następnie „przejmiesz” kontrolę, ponownie renderując stronę po stronie klienta.

Jest to skuteczne rozwiązanie, ale może mieć znaczne wady pod względem wydajności.

Główną wadą renderowania po stronie serwera z ponownym nawodnieniem 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 mogą 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ądzeniach mobilnych może to trwać kilka minut, co może dezorientować i frustrować użytkowników.

Problem z ożywianiem: jedna aplikacja w cenie dwóch

Aby JavaScript po stronie klienta mógł dokładnie przejąć kontrolę w miejscu, w którym serwer zakończył działanie, bez ponownego wysyłania żądania wszystkich danych, za pomocą których serwer wyrenderował kod HTML, większość rozwiązań do renderowania po stronie serwera serializuje odpowiedź z zależności danych interfejsu użytkownika jako tagi skryptu w dokumencie. Ponieważ duplikuje to wiele elementów HTML, ponowne renderowanie może powodować więcej problemów niż tylko opóźnioną interaktywność.

Dokument HTML zawierający zserializowany interfejs, dane wstawione w kodzie i skrypt bundle.js.

Serwer zwraca opis interfejsu aplikacji w odpowiedzi na żądanie nawigacji, ale zwraca też dane źródłowe użyte do utworzenia tego interfejsu oraz pełną kopię implementacji interfejsu, która jest następnie uruchamiana na kliencie. Interfejs użytkownika nie staje się interaktywny, dopóki bundle.js nie zostanie wczytany i wykonany.

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

Negatywny wpływ renderowania po stronie klienta na TTI.

Istnieje nadzieja na renderowanie po stronie serwera z ponowną hydratacją. W krótkiej perspektywie czasowej używanie renderowania po stronie serwera tylko w przypadku treści podlegającej zapisywaniu w pamięci podręcznej może skrócić czas TTFB i dać podobne wyniki jak renderowanie wstępne. Stopniowe, progresywne lub częściowe nawadnianie może być kluczem do zwiększenia w przyszłości możliwości wykorzystania tej techniki.

Renderowanie po stronie serwera i stopniowe przywracanie stanu

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

Renderowanie po stronie serwera z przesyłaniem strumieniowym umożliwia wysyłanie kodu HTML w częściach, które przeglądarka może stopniowo renderować w miarę ich otrzymywania. Dzięki temu użytkownicy szybciej otrzymają kod, co przyspieszy FCP. W React strumienie są asynchroniczne w renderToPipeableStream(), w przeciwieństwie do synchronicznych renderToString(), co oznacza, że dobrze radzą sobie z problemem nadmiernego obciążenia.

Warto też rozważyć progresywne ożywianie (React je wdrożył). Dzięki temu podejściu poszczególne części aplikacji renderowanej po stronie serwera są „uruchamiane” z czasem, zamiast inicjować całą aplikację jednocześnie, jak to się zwykle robi. Może to pomóc zmniejszyć ilość kodu JavaScript potrzebnego do interaktywności stron, ponieważ umożliwia odroczenie uaktualniania po stronie klienta części strony o niskim priorytecie, aby zapobiec blokowaniu wątku głównego, co pozwala na szybsze interakcje użytkownika po ich zainicjowaniu.

Progresywne ponowne nawodnienie może też pomóc uniknąć jednego z najczęstszych problemów z ponownym nawodnieniem renderowania po stronie serwera: drzewo DOM renderowane po stronie serwera jest niszczone, a następnie natychmiast odbudowywane. Najczęściej dzieje się tak, ponieważ początkowe synchroniczne renderowanie po stronie klienta wymagało danych, które nie były jeszcze gotowe, np. Promise, które nie zostało jeszcze rozwiązane.

Częściowe nawodnienie

Częściowe przywracanie stanu okazało się trudne do wdrożenia. To podejście jest rozszerzeniem progresywnego ożywiania, które analizuje poszczególne części strony (komponenty, widoki lub drzewa) i identyfikuje te, które mają niewielką interaktywność lub nie reagują na działania użytkownika. W przypadku każdej z tych w większości statycznych części odpowiedni kod JavaScript jest przekształcany w nieaktywne odwołania i elementy dekoracyjne, co zmniejsza ich rozmiar po stronie klienta niemal do zera.

Podejście polegające na częściowym ożywianiu wiąże się z własnymi problemami i kompromisami. Stwarza to pewne ciekawe wyzwania związane z pamięcią podręczną, a nawigacja po stronie klienta oznacza, że nie możemy zakładać, że wyrenderowany przez serwer kod HTML dla nieaktywnych części aplikacji jest dostępny bez pełnego wczytania strony.

Renderowanie trójmorficzne

Jeśli service worker to dla Ciebie odpowiednie rozwiązanie, rozważ renderowanie trisimorficzne. Ta technika umożliwia używanie strumieniowego renderowania po stronie serwera w przypadku początkowych lub nieopartych na JavaScript nawigacji, a następnie przekazywanie renderowania kodu HTML w przypadku nawigacji do skryptu service worker po jego zainstalowaniu. Dzięki temu buforowane komponenty i szablony będą aktualne, a nawigacja w stylu SPA umożliwi renderowanie nowych widoków w ramach tej samej sesji. To podejście sprawdza się najlepiej, gdy możesz udostępniać ten sam kod szablonu i routingu między serwerem, stroną klienta i skryptem service worker.

Renderowanie trójpostaciowe, które pokazuje, jak przeglądarka i skrypt service worker komunikują się z serwerem.

Wskazówki dotyczące SEO

Wybierając strategię renderowania stron internetowych, zespoły często biorą pod uwagę wpływ na SEO. Renderowanie po stronie serwera to popularny sposób na dostarczanie „kompletnych” treści, które mogą interpretować roboty indeksujące. Roboty indeksujące potrafią interpretować JavaScript, ale często istnieją ograniczenia dotyczące sposobu renderowania. Renderowanie po stronie klienta może działać, ale często wymaga dodatkowych testów i nakładów. Ostatnio warto też rozważyć renderowanie dynamiczne, jeśli Twoja architektura w dużej mierze zależy od JavaScriptu po stronie klienta.

Podsumowanie

Decydując się na sposób renderowania, zmierz i zrozum, jakie są Twoje wąskie gardła. Zastanów się, czy renderowanie statyczne lub renderowanie po stronie serwera nie wystarczą w Twoim przypadku. Wystarczy, że w większości przypadków będziesz wysyłać HTML z minimalną ilością JavaScriptu, aby zapewnić interaktywność. Oto przydatna infografika przedstawiająca spektrum serwer-klient:

Opcje renderowania oraz ich wady i zalety.

Środki

Dziękujemy wszystkim za opinie i inspiracje:

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