Renderowanie w internecie

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

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

Nasza wiedza na temat tego obszaru opiera się na pracy nad Chrome i rozmowach z dużymi witrynami w ciągu ostatnich kilku lat. Ogólnie rzecz biorąc, zachęcamy deweloperów do rozważenia renderowania po stronie serwera lub renderowania statycznego zamiast pełnej hydratacji.

Aby lepiej zrozumieć, jakie architektury mamy do wyboru, potrzebujemy spójnej terminologii i wspólnych ram dla każdego podejścia. Następnie 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, a nie JavaScriptu.
Renderowanie po stronie klienta (CSR)
Renderowanie aplikacji w przeglądarce z użyciem JavaScriptu do modyfikowania modelu DOM.
Renderowanie wstępne
Uruchamianie aplikacji po stronie klienta w czasie kompilacji w celu przechwycenia jej stanu początkowego jako statycznego kodu HTML. Pamiętaj, że „renderowanie wstępne” w tym sensie różni się od renderowania wstępnego 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. Hydratacja zakłada, że DOM się nie zmienia.
Ożywianie
Chociaż często używa się go w tym samym znaczeniu co hydratacja, ponowna hydratacja oznacza regularne aktualizowanie modelu DOM najnowszym stanem, również 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 żądana treść (treść artykułu itp.) staje się widoczna.
Interakcja do kolejnego wyrenderowania (INP)
Reprezentatywny wskaźnik, który ocenia, czy strona konsekwentnie szybko reaguje na dane wejściowe użytkownika.
Łączny czas blokowania (TBT)
Wskaźnik zastępczy INP, który oblicza, 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 operacji w obie strony związanych z pobieraniem danych i tworzeniem szablonów po stronie klienta, ponieważ moduł renderujący obsługuje je, zanim przeglądarka otrzyma odpowiedź.

Renderowanie po stronie serwera zwykle zapewnia szybkie FCP. Uruchamianie 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 rzadziej blokowany, interakcje użytkownika mogą być szybciej realizowane.

Ma to sens, ponieważ w przypadku renderowania po stronie serwera wysyłasz do przeglądarki użytkownika tylko tekst i linki. To podejście może sprawdzić się w przypadku różnych urządzeń i warunków sieciowych oraz umożliwia ciekawe optymalizacje przeglądarki, takie jak strumieniowe analizowanie dokumentów.

Diagram przedstawiający renderowanie po stronie serwera i wykonywanie kodu JavaScript, które wpływają 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 JavaScriptu, który obciąża procesor, zanim będą mogli korzystać z witryny. Nawet jeśli nie możesz uniknąć kodu JavaScriptu firmy zewnętrznej, użycie renderowania po stronie serwera w celu zmniejszenia kosztów własnego kodu JavaScriptu może zapewnić Ci większy budżet na pozostałe elementy. To podejście ma jednak jedną potencjalną wadę: generowanie stron na serwerze wymaga czasu, co może zwiększyć TTFB strony.

To, czy renderowanie po stronie serwera jest wystarczające dla Twojej aplikacji, zależy w dużej mierze od tego, jaki rodzaj doświadczenia chcesz stworzyć. Od dawna trwa debata 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 swoje względnie statyczne strony docelowe, a jednocześnie prefetching kod JavaScript dla stron z dużą liczbą interakcji, dzięki czemu te cięższe strony renderowane po stronie klienta mają większą szansę na szybkie wczytanie.

Wiele nowoczesnych platform, bibliotek i architektur umożliwia renderowanie tej samej aplikacji zarówno po stronie klienta, jak i serwera. Możesz używać tych technik do renderowania po stronie serwera. Jednak architektury, w których renderowanie odbywa się zarówno na serwerze, jak i na kliencie, stanowią osobną klasę rozwiązań o bardzo różnych charakterystykach wydajności i kompromisach. Użytkownicy Reacta mogą używać interfejsów API DOM serwera lub opartych na nich rozwiązań, 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 jednak jakąś formę hydratacji, więc warto wiedzieć, jakie podejście stosuje Twoje narzędzie.

Renderowanie statyczne

Renderowanie statyczne odbywa się w czasie kompilacji. Takie podejście zapewnia szybkie 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ż stały, szybki TTFB, ponieważ kod HTML strony nie musi być generowany dynamicznie na serwerze. Zasadniczo renderowanie statyczne oznacza wcześniejsze wygenerowanie osobnego pliku HTML dla każdego adresu URL. Dzięki wygenerowanym wcześniej odpowiedziom HTML możesz wdrażać statyczne renderowanie 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 z renderowaniem statycznym.

Rozwiązania do renderowania statycznego są dostępne w różnych formach 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 witryn statycznych, takie jak 11ty, Jekyll i Metalsmith, wykorzystują ich statyczny charakter, zapewniając podejście bardziej 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, oraz w przypadku witryn z dużą liczbą unikalnych stron.

Użytkownicy Reacta mogą znać Gatsby, Next.js static export lub Navi, które ułatwiają tworzenie stron z komponentów. Renderowanie statyczne i renderowanie wstępne działają jednak inaczej: strony renderowane statycznie są interaktywne bez konieczności wykonywania dużej ilości kodu JavaScript po stronie klienta, natomiast renderowanie wstępne poprawia FCP aplikacji jednostronicowej, którą trzeba uruchomić na kliencie, aby strony były naprawdę 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 działa bez JavaScriptu. Wstępnie wyrenderowane strony mogą nadal mieć pewne 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 kodu JavaScript jest pobierane, zanim strona stanie się interaktywna. Renderowanie wstępne zwykle wymaga więcej kodu JavaScript, aby stać się interaktywne, a ten kod JavaScript jest zwykle bardziej złożony niż podejście polegające na progresywnym ulepszaniu 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 związanymi z zasobami obliczeniowymi. Wiele rozwiązań do renderowania po stronie serwera nie opróżnia pamięci podręcznej, opóźnia TTFB lub podwaja ilość wysyłanych danych (np. stany wbudowane używane przez JavaScript na kliencie). W React renderToString() może działać wolno, ponieważ jest synchroniczny i jednowątkowy. Nowsze interfejsy API DOM serwera React obsługują strumieniowanie, dzięki czemu początkowa część odpowiedzi HTML może szybciej dotrzeć do przeglądarki, podczas gdy reszta jest nadal generowana na serwerze.

Aby „prawidłowo” skonfigurować renderowanie po stronie serwera, trzeba znaleźć lub utworzyć rozwiązanie do buforowania komponentów, zarządzania wykorzystaniem pamięci, stosowania technik zapamiętywania i innych kwestii. Często przetwarzasz lub przebudowujesz tę samą aplikację dwukrotnie – raz po stronie klienta, a raz po stronie serwera. Renderowanie po stronie serwera, które pozwala szybciej wyświetlać treści, niekoniecznie oznacza mniej pracy. Jeśli po stronie klienta jest dużo pracy po otrzymaniu odpowiedzi HTML wygenerowanej przez serwer, może to nadal prowadzić do wyższych wartości TBT i INP w witrynie.

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

Renderowanie po stronie serwera może też wymagać podjęcia ciekawych decyzji podczas tworzenia PWA. Czy lepiej jest używać buforowania całej strony przez skrypt service worker, czy renderować 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, tworzenie szablonów i routing są obsługiwane po stronie klienta, a nie serwera. W efekcie z serwera na urządzenie użytkownika jest przesyłanych więcej danych, co wiąże się z pewnymi kompromisami.

Renderowanie po stronie klienta może być trudne do utworzenia i utrzymania na urządzeniach mobilnych. Jeśli poświęcisz trochę pracy na utrzymanie niskiego budżetu JavaScriptu i zapewnienie wartości w jak najmniejszej liczbie operacji dwukierunkowych, możesz uzyskać renderowanie po stronie klienta, które będzie niemal tak 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ż rozważenie użycia 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 ilość wymaganego kodu JavaScript zwykle rośnie wraz z rozwojem aplikacji, co może mieć wpływ na INP strony. Staje się to szczególnie trudne, gdy dodawane są nowe biblioteki JavaScriptu, polyfille i kod innych firm, które konkurują o moc obliczeniową i często muszą zostać przetworzone, zanim będzie można wyrenderować zawartość strony.

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

W przypadku aplikacji jednostronicowych określenie podstawowych elementów interfejsu użytkownika, które są wspólne dla większości stron, pozwala zastosować technikę buforowania powłoki aplikacji. W połączeniu z skryptami service worker może to znacznie poprawić postrzeganą wydajność przy kolejnych wizytach, ponieważ strona może bardzo szybko wczytać kod HTML powłoki aplikacji i zależności z CacheStorage.

Ponowne nawodnienie łączy renderowanie po stronie serwera i po stronie klienta

Hydratacja to podejście, które pozwala ograniczyć kompromisy między renderowaniem po stronie klienta i po stronie serwera, ponieważ wykorzystuje oba te rozwiązania. Żądania nawigacji, takie jak wczytywanie lub ponowne wczytywanie całej strony, są obsługiwane przez serwer, który renderuje aplikację do formatu HTML. Następnie JavaScript i dane używane do renderowania są umieszczane w dokumencie wynikowym. Jeśli zostanie to zrobione prawidłowo, można uzyskać szybkie renderowanie FCP, podobnie jak w przypadku renderowania po stronie serwera, a następnie „przejąć” renderowanie 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ą wydawać się 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 potrwać kilka minut, co może być dla użytkownika frustrujące.

Problem z ponownym nawodnieniem: jedna aplikacja w cenie dwóch

Aby kod JavaScript po stronie klienta mógł dokładnie przejąć zadanie od serwera bez ponownego żą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 jako tagi skryptu w dokumencie. Ponieważ powoduje to powielenie dużej ilości kodu HTML, ponowne nawodnienie może powodować więcej problemów niż tylko opóźnioną interaktywność.

Dokument HTML zawierający zserializowany interfejs, dane wbudowane i skrypt bundle.js.

W odpowiedzi na żądanie nawigacji serwer zwraca opis interfejsu aplikacji, ale także dane źródłowe użyte do utworzenia tego interfejsu oraz pełną kopię implementacji interfejsu, która następnie uruchamia się na kliencie. Interfejs nie staje się interaktywny, dopóki nie zakończy się wczytywanie i wykonywanie skryptu bundle.js.

Wskaźniki skuteczności zebrane z prawdziwych witryn korzystających z renderowania po stronie serwera i rehydratacji 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.

Nadzieję daje renderowanie po stronie serwera z rehydratacją. W krótkiej perspektywie używanie renderowania po stronie serwera tylko w przypadku treści podlegającej zapisywaniu w pamięci podręcznej może skrócić TTFB, dając podobne wyniki jak renderowanie wstępne. Kluczem do zwiększenia przydatności tej techniki w przyszłości może być przyrostowe, stopniowe lub częściowe ponowne nawadnianie.

Strumieniowe renderowanie po stronie serwera i progresywne ponowne nawadnianie

W ciągu ostatnich kilku lat renderowanie po stronie serwera znacznie się rozwinęło.

Strumieniowe renderowanie po stronie serwera umożliwia wysyłanie kodu HTML we fragmentach, które przeglądarka może stopniowo renderować w miarę ich otrzymywania. Dzięki temu znaczniki mogą szybciej docierać do użytkowników, co przyspiesza FCP. W React strumienie są asynchroniczne w renderToPipeableStream(), a nie synchroniczne jak w renderToString(), co oznacza, że dobrze radzą sobie z przeciążeniem.

Warto też rozważyć progresywne ponowne nawadnianie (zostało ono zaimplementowane w React). W tym podejściu poszczególne elementy aplikacji renderowanej po stronie serwera są „uruchamiane” z czasem, a nie wszystkie naraz, jak to się obecnie dzieje. Może to pomóc zmniejszyć ilość kodu JavaScript potrzebnego do zapewnienia interaktywności stron, ponieważ pozwala odroczyć uaktualnianie po stronie klienta części strony o niskim priorytecie, aby nie blokowały one wątku głównego. Dzięki temu interakcje użytkownika mogą następować szybciej po ich zainicjowaniu.

Progresywne ponowne nawadnianie może też pomóc uniknąć jednego z najczęstszych problemów z ponownym nawadnianiem podczas renderowania po stronie serwera: drzewo DOM wyrenderowane po stronie serwera zostaje zniszczone, a następnie natychmiast odbudowane, 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óry nie został jeszcze rozwiązany.

Częściowe ożywianie

Częściowe ponowne nawodnienie okazało się trudne do wdrożenia. To podejście jest rozszerzeniem progresywnego ponownego nawadniania, które analizuje poszczególne elementy 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 części, które są w większości statyczne, odpowiedni kod JavaScript jest przekształcany w odwołania i funkcje dekoracyjne, co zmniejsza ich wpływ po stronie klienta do niemal zera.

Podejście polegające na częściowym ponownym nawodnieniu wiąże się z pewnymi problemami i kompromisami. Powoduje to pewne problemy z buforowaniem, a nawigacja po stronie klienta oznacza, że nie możemy zakładać, że kod HTML renderowany po stronie serwera dla nieaktywnych części aplikacji jest dostępny bez pełnego wczytania strony.

Renderowanie triizomorficzne

Jeśli możesz używać skryptów service worker, rozważ renderowanie trisomorficzne. Ta technika umożliwia korzystanie z renderowania strumieniowego po stronie serwera w przypadku nawigacji początkowej lub bez użycia JavaScriptu, a następnie przejęcie przez skrypt service worker renderowania kodu HTML na potrzeby nawigacji po zainstalowaniu. Dzięki temu komponenty i szablony w pamięci podręcznej są aktualne, a nawigacja w stylu SPA umożliwia renderowanie nowych widoków w tej samej sesji. To podejście sprawdza się najlepiej, gdy możesz udostępniać ten sam kod szablonów i routingu między serwerem, stroną klienta i skryptem service worker.

Renderowanie trisomorficzne, pokazujące przeglądarkę i skrypt service worker komunikujące się z serwerem.

Kwestie związane z 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 roboty mogą zinterpretować. Roboty rozumieją JavaScript, ale często mają ograniczenia dotyczące renderowania. Renderowanie po stronie klienta może działać, ale często wymaga dodatkowych testów i nakładów pracy. Ostatnio renderowanie dynamiczne stało się opcją wartą rozważenia, jeśli Twoja architektura w dużym stopniu zależy od kodu JavaScript po stronie klienta.

Podsumowanie

Podejmując decyzję o podejściu do renderowania, zmierz i zrozum, jakie są wąskie gardła. Zastanów się, czy renderowanie statyczne lub renderowanie po stronie serwera nie pozwoli Ci osiągnąć większości celów. Możesz wysyłać głównie kod HTML z minimalną ilością kodu JavaScript, aby zapewnić interaktywność. Oto przydatna infografika przedstawiająca spektrum serwer-klient:

Opcje renderowania oraz ich zalety i wady.

Środki

Dziękujemy wszystkim za opinie i inspiracje:

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