Jedną z najważniejszych decyzji, jakie muszą podjąć deweloperzy stron internetowych, jest to, gdzie w aplikacji zaimplementować 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, w ramach której w ciągu ostatnich kilku lat rozmawialiśmy z dużymi witrynami. 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 skutecznoś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 momencie kompilacji, aby przechwycić jej stan początkowy jako statyczny kod HTML.
- 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 nie ulega zmianie.
- Nawodnienie
- Chociaż często używa się tego terminu w tym samym znaczeniu co nawodnienie, rehydracja oznacza regularne aktualizowanie DOM o najnowszy stan, w tym po początkowym nawodnieniu.
Wyniki
- 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 konsekwentnie szybko reaguje na dane wprowadzane przez użytkownika.
- Wskaźnik Total Blocking Time (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 kodu JavaScript do klienta. Pomaga to zmniejszyć TTBT strony, co może również prowadzić do niższego INP, ponieważ wątek główny nie jest tak często blokowany podczas ładowania 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 dotyczących urządzeń i sieci oraz umożliwia ciekawe optymalizacje przeglądarki, takie jak strumieniowe analizowanie dokumentów.
Renderowanie po stronie serwera zmniejsza ryzyko, że użytkownicy będą musieli czekać na uruchomienie skryptu JavaScript, który obciąża procesor, zanim będą mogli korzystać z Twojej witryny. Nawet jeśli nie możesz uniknąć kodu JavaScript 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 elementy. 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 tego, jaki rodzaj interakcji chcesz stworzyć. 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 nie w przypadku innych. Niektóre witryny z powodzeniem stosują techniki renderowania hybrydowego. Na przykład Netflix renderuje po stronie serwera stosunkowo statyczne strony docelowe, a prefetching JavaScript dla stron wymagających 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 po stronie klienta, jak i serwera. 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 kliencie, stanowią jednak odrębną klasę rozwiązań o bardzo różnych charakterystykach wydajności i kompromisach. Użytkownicy Reacta mogą używać serwerowych interfejsów DOM API 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 jakąś formę nawadniania, więc sprawdź, jakie metody 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łą szybkość 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 wygenerowanym z wyprzedzeniem odpowiedziom HTML możesz wdrażać statyczne wersje na wielu sieciach CDN, aby korzystać z pamięci podręcznej na urządzeniach brzegowych.
Rozwiązania do renderowania statycznego są bardzo różne. Narzędzia takie jak Gatsby zostały zaprojektowane tak, aby deweloperzy mieli wrażenie, że ich aplikacja jest renderowana dynamicznie, a nie generowana w ramach procesu kompilacji. Narzędzia do generowania statycznych witryn, takie jak 11ty, Jekyll i Metalsmith, wykorzystują statyczny charakter witryn i oferują 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. Wszystkie te narzędzia 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, natomiast renderowanie wstępne poprawia FCP aplikacji jednostronicowej, która musi zostać uruchomiona po stronie 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. Wstępnie renderowane strony mogą nadal mieć niektóre podstawowe funkcje, takie jak linki z wyłączonym JavaScriptem, ale większość strony jest nieaktywna.
Innym przydatnym testem jest użycie ograniczania przepustowości 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 JavaScriptu, aby stać się interaktywne, a ten 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ć powoli, ponieważ jest synchroniczna i jednowątkowa.
Nowsze interfejsy API DOM serwera React obsługują przesyłanie strumieniowe, dzięki czemu początkowa część odpowiedzi HTML może szybciej dotrzeć do przeglądarki, podczas gdy reszta jest nadal generowana na serwerze.
Prawidłowe renderowanie po stronie serwera może wymagać znalezienia lub stworzenia 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ę dwukrotnie: 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 do wykonania po otrzymaniu 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ż wyświetlanie statycznych treści. Jeśli możesz wykonać dodatkową pracę, renderowanie po stronie serwera w połączeniu z buforowaniem HTML może 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 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ć pamięci podręcznej service worker na całą stronę, 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 z serwera więcej danych, co wiąże się z określonymi kompromisami.
Renderowanie po stronie klienta może być trudne do szybkiego wdrożenia i utrzymania na urządzeniach mobilnych.
Jeśli poświęcisz trochę czasu na utrzymanie niewielkiego budżetu na JavaScript i dostarczanie wartości w jak najmniejszej liczbie cykli, możesz sprawić, że renderowanie po stronie klienta będzie niemal 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.
Główną wadą renderowania po stronie klienta jest to, że wraz z rozwojem aplikacji rośnie ilość wymaganego kodu JavaScript, co może wpływać na INP strony. Staje się to szczególnie trudne, gdy dodawane są nowe biblioteki JavaScript, 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 skrócić czas do interakcji i czas interakcji z najdłuższym opóźnieniem podczas wczytywania strony, a także leniwe wczytywanie JavaScriptu, aby dostarczać użytkownikowi tylko to, czego potrzebuje, i tylko wtedy, gdy 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, zidentyfikowanie podstawowych części interfejsu użytkownika wspólnych 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 pozwala zminimalizować kompromisy między renderowaniem po stronie klienta a renderowaniem 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” 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ą 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 dezorientuje i frustruje użytkowników.
Problem z ponownym nawodnieniem: 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 renderował 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 nawodnienie może powodować więcej problemów niż tylko opóźniona interaktywność.
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 staje się interaktywny dopiero po zakończeniu wczytywania i wykonywania bundle.js.
Dane o skuteczności zebrane z prawdziwych witryn korzystających z renderowania po stronie serwera i ponownego nawodnienia wskazują, że rzadko jest to najlepsza opcja. Najważniejszym powodem jest wpływ na wrażenia użytkowników, gdy strona wygląda na gotową, ale żadna z jej interaktywnych funkcji nie działa.
Jest nadzieja na renderowanie po stronie serwera z ponownym nawodnieniem. W krótkiej perspektywie używanie renderowania po stronie serwera tylko w przypadku treści, które można w dużym stopniu przechowywać w pamięci podręcznej, może skrócić czas TTFB i dać podobne wyniki jak wstępne renderowanie. Stopniowe, progresywne lub częściowe przywracanie 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 bardzo się rozwinęło.
Renderowanie po stronie serwera z przesyłaniem strumieniowym umożliwia wysyłanie kodu HTML w porcjach, które przeglądarka może stopniowo renderować w miarę ich otrzymywania. Dzięki temu znaczniki będą szybciej docierać do użytkowników, co przyspieszy FCP. W React strumienie są asynchroniczne w renderToPipeableStream(), w przeciwieństwie do synchronicznych renderToString(), co oznacza, że dobrze radzą sobie z ograniczaniem przepustowości.
Warto też rozważyć progresywne ponowne nawodnienie (React je wdrożył). W tym podejściu poszczególne części aplikacji renderowanej po stronie serwera są „uruchamiane” z czasem, a nie tak jak obecnie – cała aplikacja jest inicjowana jednocześnie. Może to pomóc zmniejszyć ilość kodu JavaScript potrzebnego do interaktywnego działania stron, ponieważ umożliwia odroczenie uaktualniania po stronie klienta części strony o niskim priorytecie, aby zapobiec blokowaniu wątku głównego i umożliwić interakcje użytkownika zaraz po ich zainicjowaniu.
Progresywne ponowne nawodnienie może też pomóc uniknąć jednego z najczęstszych problemów związanych 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 ponownego nawadniania, 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 przywracaniu nawodnienia 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 renderowany przez serwer kod HTML dla nieaktywnych części aplikacji jest dostępny bez pełnego wczytania strony.
Renderowanie trójmorficzne
Jeśli service workers są dla Ciebie opcją, rozważ renderowanie trisomorphic. 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 usługi Service Worker po jej zainstalowaniu. Dzięki temu komponenty i szablony w pamięci podręcznej 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 service workerem.
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 wybór, jeśli chodzi o zapewnienie „kompletnego” wyglądu strony, który mogą interpretować roboty indeksujące. Roboty indeksujące potrafią interpretować JavaScript, ale często mają ograniczenia w zakresie 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ć dynamiczne renderowanie, jeśli Twoja architektura w dużej mierze zależy od JavaScriptu po stronie klienta.
W razie wątpliwości skorzystaj z narzędzia do testowania optymalizacji mobilnej, aby sprawdzić, czy wybrane podejście spełnia Twoje oczekiwania. Wyświetla on podgląd strony widzianej przez robota Google, zserializowaną treść HTML znalezioną po wykonaniu kodu JavaScript oraz błędy napotkane podczas renderowania.
Podsumowanie
Decydując się na konkretne podejście do 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:
Środki: {:#credits}
Dziękujemy wszystkim za opinie i inspiracje:
Jeffrey Posnick, Houssein Djirdeh, Shubhie Panicker, Chris Harrelson i Sebastian Markbåge.