Jakiś czas temu stworzyliśmy zestawienie najpopularniejszych obecnie frameworków i porównaliśmy ich funkcjonalności. Tekst ten zawierał podstawowe dane, rozszerzone o komentarze naszych specjalistów. Dziś chcemy wrócić do jednego z frameworków, a mianowicie do Next.js i pokazać, jakie możliwości renderowania danych oferuje to rozwiązanie. Przy okazji zrobimy małe porównanie NextJs oraz ReactJs, bowiem da to szersze spojrzenie na oba frameworki.

Co wyróżnia Next.js?

NextJs jest frameworkiem, który zapewnia nam wszystkie funkcje React. Next bardzo ułatwia naszą pracę, a elementy takie jak renderowanie po stronie serwera, wbudowane ścieżki API, automatyczne dzielenie kodu, optymalizacja zasobów, wstępne renderowanie, internacjonalizacja i wiele więcej, to potwierdzają. Aby dowiedzieć się więcej o możliwościach tego frameworka, warto odwiedzić jego stronę.

NextJs pozwala na renderowanie zasobów następującymi metodami: Static Site Generation (SSG), Server Side Rendering (SSR), Client Side Rendering (CSR) i Incremental Static Regeneration (ISR). Warto dodać tutaj, że ISR to w zasadzie zaawansowana wersja SSG, której możemy użyć do rozwiązania problemów z nieaktualnymi danymi w SSG.

Czym jest DOM?

DOM jest zasadniczo opartym na obiektach odpowiednikiem przeglądanej strony internetowej. To zestaw węzłów, każdy węzeł odnosi się do określonego elementu na stronie www. Krótko mówiąc, DOM przypomina trochę drzewo, a każda gałąź odpowiada elementowi na stronie. Warto spojrzeć na te dwa przydatne linki: “Introduction to the DOM” oraz “Live DOM viewer”.

Rozwiązaniem było zastosowanie React.js, by zamiast odtwarzać cały DOM, stworzyć vDOM, czyli wirtualny odpowiednik DOM'u, który pozwalał na aktualizowanie tylko niezbędnych fragmentów rzeczywistego DOM. Dało to ogromny wzrost wydajności w porównaniu z innymi frameworkami.

Client Side Rendering (CSR) w React.js vs. Server Side Rendering (SSR) w Next.js

Tutaj warto pokazać różnice, jakie pojawiają się podczas renderowania CSR w ReactJs oraz SSR w NextJs.

Podczas odwiedzin przez użytkownika aplikacji zbudowanej w React (CSR), w tle zachodzą następujące procesy:

  1. Do klienta (przeglądarki) wysyłana jest prawie pusta odpowiedź, która jest minimalnym dokumentem HTML. Ten dokument nie zawiera nic poza linkami do fragmentów JavaScript znajdujących się na serwerze. Aby strona w całości się wyświetliła potrzeba chwili czasu, bowiem nie otrzymuje ona na tym etapie żadnej informacji z wyjątkiem wspomnianego prawie pustego dokumentu HTML.
  2. W drugim kroku z serwera pobierany jest pakiet JavaScript. Zwykle jest on pobierany w całości, chyba że zaimplementowane zostało manualne podzielenie kodu - funkcja (React.lazy()).
  3. Przeglądarka wykonuje komendy JavaScript (React) i renderuje za pomocą (createRoot(rootEl).render()) zawartość na ekranie. Wypełnia wcześniej pusty dokument HTML zawierający węzeł główny. W tym momencie strona staje się w pełni widoczna i interaktywna.

W przypadku Next.js, możemy wykorzystać Server Side Rendering (SSR) dla renderowania naszych aplikacji. Jako ciekawostkę można dodać, że w Vanilla React.js możemy również zaimplementować komponenty SSR, używając czegoś, co nazywa się ReactDomServer, ale wymaga to dodatkowej pracy i konfiguracji.

Podczas odwiedzin przez użytkownika aplikacji zbudowanej w Next.js (SSR), do serwera wysyłane jest żądanie dotyczące konkretnej strony. Nie wysyła się całego pakietu JavaScript do klienta, a całość wygląda to mniej więcej następująco:

  1. Żądanie trafia na serwer klienta i tam zaczyna się budować dokument HTML. Trwa to dosłownie milisekundy, oczywiście czas ten zależy od szybkości serwerów.
  2. Wygenerowana strona HTML jest odsyłana z powrotem do przeglądarki wraz z powiązanym kodem JavaScript niezbędnym dla działania strony. Przeglądarka pobiera dokument HTML i wyświetla go dla nas. W tym momencie strona jest w całości widoczna dla użytkownika, jednakże nie można z nią jeszcze wchodzić w interakcje. Takie zawieszenie trwa około kilku milisekund.
  3. W ostatnim kroku przeglądarka pobiera JavaScript z serwera, a strona staje się interaktywna.

Różnice występujące pomiędzy obiema metodami

  • W przypadku React następuje minimalne opóźnienie, ponieważ na początku przeglądarka otrzymuje prawie pusty dokument. W Next dokument HTML jest w pełni wyrenderowany.
  • Wykorzystanie zasobów serwera jest zwykle wysokie dla Next.js, ponieważ to serwer wykonuje wszystkie obciążające system zadania.

Renderowanie wstępne, czyli pre-renderowanie w Next.js

Next.js oferuje on różne sposoby renderowania stron internetowych, w tym:

Server-side rendering (SSR): Renderowanie strony na serwerze przed wysłaniem jej do przeglądarki. Możemy więc wykorzystać technologię JS np. React i Server Side Rendering, aby uzyskać o wiele lepsze doświadczenie użytkownika na stronie.

Przykładowy kod:

import { createClient } from "contentful";
import { Post } from "../../components/Post";

export default function Index({ blog }) {
  return (
    <>
      <h1 style={{ textAlign: "center" }}>{blog.text}</h1>
      <div
        style={{
          display: "flex",
          justifyContent: "center",
          alignItems: "center",
        }}
      >
        {blog.posts.map((post) => (
          <Post key={post.fields.text} post={post} />
        ))}
      </div>
      <div
        style={{ display: "flex", justifyContent: "center", marginTop: "30px" }}
      >
        <a
          href={"/"}
          style={{ padding: "20px 30px", border: "1px solid white" }}
        >
          Back to Home!
        </a>
      </div>
    </>
  );
}

export const getServerSideProps = async () => {
  const client = createClient({
    space: process.env.CONTENTFUL_SPACE_ID,
    accessToken: process.env.CONTENTFUL_ACCESS_TOKEN,
  });

  const res = await client.getEntries({
    content_type: "blog",
  });

  return {
    props: {
      blog: res.items[0].fields,
    },
  };
};

Static site generation (SSG): Generowanie statycznej wersji strony na etapie budowania aplikacji. Strona jest wtedy dostępna jako zestaw plików HTML, CSS i JavaScript, które można hostować na dowolnym serwerze.

Przykładowy kod:

import { createClient } from "contentful";
import { Post } from "../../components/Post";
import { GetStaticProps } from "next";

export default function Index({ blog }) {
  return (
    <>
      <h1 style={{ textAlign: "center" }}>{blog.text}</h1>
      <div
        style={{
          display: "flex",
          justifyContent: "center",
          alignItems: "center",
        }}
      >
        {blog.posts.map((post) => (
          <Post key={post.fields.text} post={post} />
        ))}
      </div>
      <div
        style={{ display: "flex", justifyContent: "center", marginTop: "30px" }}
      >
        <a
          href={"/"}
          style={{ padding: "20px 30px", border: "1px solid white" }}
        >
          Back to Home!
        </a>
      </div>
    </>
  );
}

export const getStaticProps: GetStaticProps = async () => {
  const client = createClient({
    space: process.env.CONTENTFUL_SPACE_ID,
    accessToken: process.env.CONTENTFUL_ACCESS_TOKEN,
  });

  const res = await client.getEntries({
    content_type: "blog",
  });

  return {
    props: {
      blog: res.items[0].fields,
    },
  };
};

Incremental static regeneration (ISR): Mechanizm pozwalający na dynamiczne aktualizowanie statycznej strony przy użyciu webhooków lub innych sposobów wyzwalania regeneracji. To pozwala na dynamiczne aktualizowanie strony za pomocą SSG, ale z większymi ograniczeniami niż przy użyciu SSR. W podstawowej wersji ISR działa o ustawiony w kodzie czas. Więcej na ten temat znaleźć można w dokumentacji Next.

Przykładowy kod:

import { createClient } from "contentful";
import { Post } from "../../components/Post";
export default function Index({ blog }) {
  return (
    <>
      <h1 style={{ textAlign: "center" }}>{blog.text}</h1>
      <div
        style={{
          display: "flex",
          justifyContent: "center",
          alignItems: "center",
        }}
      >
        {blog.posts.map((post) => (
          <Post key={post.fields.text} post={post} />
        ))}
      </div>
      <div
        style={{ display: "flex", justifyContent: "center", marginTop: "30px" }}
      >
        <a
          href={"/"}
          style={{ padding: "20px 30px", border: "1px solid white" }}
        >
          Back to Home!
        </a>
      </div>
    </>
  );
}

export const getStaticProps = async () => {
  const client = createClient({
    space: process.env.CONTENTFUL_SPACE_ID,
    accessToken: process.env.CONTENTFUL_ACCESS_TOKEN,
  });

  const res = await client.getEntries({
    content_type: "blog",
  });

  return {
    props: {
      blog: res.items[0].fields,
    },
    revalidate: 10,
  };
};

Client-side rendering (CSR): Renderowanie strony na kliencie przy użyciu przeglądarki internetowej. To pozwala na dynamiczne aktualizowanie strony bez konieczności odświeżania całej strony, ale może być wolniejsze niż SSR lub SSG, ponieważ wymaga przetworzenia całego kodu JavaScript przez przeglądarkę.

Przykładowy kod:

import { createClient } from "contentful";
import { useEffect, useState } from "react";
import { Post } from "../../components/Post";

const getBlockApi = async () => {
  const client = createClient({
    space: process.env.CONTENTFUL_SPACE_ID,
    accessToken: process.env.CONTENTFUL_ACCESS_TOKEN,
  });
  return await client.getEntries({
    content_type: "blog",
  });
};

const Index = () => {
  const [blog, setBlog] = useState(null);

  useEffect(() => {
    getBlockApi().then((res) => {
      setBlog(res.items[0].fields);
    });
  }, []);

  return blog ? (
    <>
      <h1 style={{ textAlign: "center" }}>{blog.text}</h1>
      <div
        style={{
          display: "flex",
          justifyContent: "center",
          alignItems: "center",
        }}
      >
        {blog.posts.map((post) => (
          <Post key={post.fields.text} post={post} />
        ))}
      </div>
      <div
        style={{ display: "flex", justifyContent: "center", marginTop: "30px" }}
      >
        <a
          href={"/"}
          style={{ padding: "20px 30px", border: "1px solid white" }}
        >
          Back to Home!
        </a>
      </div>
    </>
  ) : (
    <span>Loading...</span>
  );
};

export default Index;

Next.js umożliwia łatwe wybieranie między tymi różnymi sposobami renderowania za pomocą odpowiednich opcji konfiguracji i komponentów, takich jak getStaticProps, getServerSideProps i getStaticPaths. Możesz wybrać sposób renderowania, który najlepiej pasuje do potrzeb Twojej wdrażanej

Porównanie pre-renderowania w pigułce

Poniżej znajduje się grafika, która pokazuje, jak wygląda build naszej strony, w zależności od wykorzystania różnych typów generowania.

Czasami zdarza się, że nie potrafimy wybrać renderowania odpowiedniego dla naszych potrzeb. Dlatego dobrze jest porównać na wykresach 5 najważniejszych obszarów renderowania. Są to:

  • Build time — im niższa wartość, tym lepiej.
  • Suitable for Dynamic Content — jeśli ta wartość jest wysoka, to model ten będzie bardziej odpowiedni dla aplikacji z dużą ilością zawartości dynamicznej.
  • Search Engine Optimization - wartość ma ogromne znaczenie w pozycjonowaniu, im wyższa, tym lepiej.
  • Page Serve/Render Time — czas potrzebny na renderowanie całej strony w przeglądarce internetowej. Im krótszy, tym lepiej.
  • Most Recent Content — wskazanie, że zawsze dostarczana jest najnowsza zawartość. Im większy zakres, tym lepiej. Static Server / App Server — pokazuje, czy ta technika wymaga serwera statycznego lub serwera aplikacji. Zwykle serwery statyczne zużywają znacznie mniej zasobów niż serwery aplikacji.

W 'merce korzystamy z opcji renderowania po stronie serwera (SSR) w połączeniu z (CSR). Co najważniejsze, te podejścia nie muszą się wykluczać i często znakomicie się uzupełniają. Taki wybór był podyktowany specyfiką produktu dostarczanego przez naszą markę. Mowa o autorskiej platformie e-commerce typu enterprise. Merce dostarcza platformę ecommerce. Nasi klienci posiadają rozbudowane sklepy internetowe, oparte na naszym autorskim rozwiązaniu, które wymagają, aby treści na ich witrynach były widoczne dla klientów w jak najkrótszym czasie. Dodatkowo w przypadku e-commerce bardzo ważne są wskaźniki związane z SEO. Strony te muszą być czytelne dla robotów Google. Dla dynamicznego contentu w sklepach, w połączeniu z globalnym cache (redis) wybrane renderowanie sprawdza się najlepiej.

Sklep New Balance wdrożony przez Merce.com wykorzystujący renderowanie po stronie serwera (SSR) w połączeniu z (CSR)

Jaka metoda renderowania wybrać?

Każda z tych metod jest odpowiednia dla różnych sytuacji. CSR jest przydatny na stronach, które potrzebują odświeżenia danych, gdzie nie musimy kłaść nacisku na silne SEO. SSR jest również świetny dla tego typu stron, które zużywają dynamiczne dane i jest bardziej przyjazny dla SEO.

SSG jest odpowiedni dla stron, których dane są w większości statyczne, podczas gdy ISG jest najlepszy dla stron zawierających dane, które chcesz zaktualizować w odstępach czasu. SSG i ISG są świetne pod względem wydajności i SEO. Decydując się na wybrany model renderowania warto brać pod uwagę to, jakie chcemy osiągnąć cele.

Tekst powstał przy współudziale Daniela Jaworskiego