Всем привет, меня зовут Хаджимурад, занимаюсь фронтенд разработкой уже 3,5 года и за это время успел поработать на многих проектах: интернет магазин, панели администрирования, проекты для университета. Сейчас работаю на проекте в банке и сегодня хочу поделится с вами своим опытом.

В статье на примере простого сайта, постараюсь описать три способа рендеринга приложений, раскрыть их плюсы и минусы, и на практических примерах провести сравнение производительности и размеров приложения. Материал больше подойдёт начинающим фронтенд-разработчикам, или тем, кто хочет познакомиться c CSR, SSG и SSR. Для лучшего понимания от вас потребуются начальные знания React, менеджеров пакетов npm или yarn.

Представим, что мы решили сделать сайт посвященный фильмам. На нём будет список фильмов и их описание с постерами и рейтингом. Сначала сверстаем его используя HTML и CSS.

<!doctype html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport"
          content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0">
    <meta http-equiv="X-UA-Compatible" content="ie=edge">
    <link rel="stylesheet" href="./style.css">
    <title>Киноафиша</title>
</head>
<body>
<section>
    <h1>Киноафиша</h1>
    <p>
        Киноафиша – это наиболее полная информация о кино и кинотеатрах.
        <br />
        У нас вы найдёте подробные сведения о фильмах, новостях мира кино и кинозвёзд
    </p>
    <div class="card">
        <img
            class="card__poster"
            src="https://avatars.mds.yandex.net/get-ott/200035/2a0000017f97fb04bd9cc3a1c5d58b1505b6/440x660"
            alt="Гнев человеческий"
        >
        <p class="card__title">Гнев человеческий</p>
        <span class="card__rating">7.6</span>
    </div>
</section>
</body>
</html>

Примечание. Кодовая база примеров из статьи доступна на GitHub.

Сайт будет выглядеть так.

Наш сайт посвящен фильмам, и он должен постоянно обновляться, ведь регулярно выходят новые картины, а значит, нам нужно получать актуальный список фильмов из БД. Для этого построим простой API и перепишем приложение на современную библиотеку или фреймворк, в данном случае на React — как самый популярный инструмент для создания пользовательских интерфейсов. 

И здесь появляется CSR.

CSR: Client Side Rendering

Примечание: Для данного примера воспользуемся create-react-app.

Установим базовый шаблон с помощью команды npx create-react-app my-app(у вас уже должен быть установлен Node.js). Перенесём верстку в компоненту App и добавим логику получения списка фильмов.

Примечание: для упрощения показа логики на фронте не будет настоящего API, а хардкод из заранее подготовленного файла.

function App() {
  const [isLoading, setIsLoading] = useState(false);
  const [movies, setMovies] = useState([])

  useEffect(() => {
    setIsLoading(true);
    fetch('./db/movies.json')
        .then(response => response.json())
        .then(data => {
            setMovies(data);
            setIsLoading(false);
         })
  }, [])

  return (
    <>
      <section>
        <h1>Киноафиша</h1>
        <p>
          Киноафиша – это наиболее полная информация о кино и кинотеатрах.
          <br />
          У нас вы найдёте подробные сведения о фильмах, новостях мира кино и кинозвёзд
        </p>
          <div className="movies">
              { isLoading ?
                  "Загрузка..."
                  : movies.map(movie =>
                      <Card key={movie.title} url={movie.url} title={movie.title} rating={movie.rating} />)
              }
          </div>
      </section>
    </>
  );
}

function Card({ url, title, rating }) {
  return <div className="card">
    <img className="card__poster"
      src={ url }
      alt={ title }
     />
    <p className="card__title">{ title }</p>
      <span className="card__rating">{ rating }</span>
  </div>
}

А теперь командой npm run build сделаем сборку приложения, чтобы получить продакшн сборку, и увидим папку /dist файл index.html со следующим содержанием.

<!doctype html>
<html lang="en">
  <head>
    <meta charset="utf-8" />
    <link rel="icon" href="/favicon.ico" />
    <meta name="viewport" content="width=device-width,initial-scale=1" />
    <meta name="theme-color" content="#000000" />
    <meta name="description" content="Web site created using create-react-app" />
    <link rel="apple-touch-icon" href="/logo192.png" />
    <link rel="manifest" href="/manifest.json" />
    <title>React App</title>
    <script defer="defer" src="/static/js/main.e1348a94.js"></script>
    <link href="/static/css/main.6ba66afe.css" rel="stylesheet">
  </head>
  <body><noscript>You need to enable JavaScript to run this app.</noscript>
    <div id="root"></div>
  </body>
</html>

Как можно заметить в body пустой div с id=”root”, но где же наша верстка?

Всё дело в том, что React — это JavaScript библиотека, а это значит, что код может быть исполнен в среде Node.js или в браузере. В нашем случае это браузер.  И если выключить JS в браузере, то мы увидим только пустую страницу, потому что дерево рисуется скриптом.

И принцип работы React построен следующим образом:

  • при открытии сайта браузер отправляет запрос на сервер;

  • тот в ответ возвращает пустую страницу index.html, в которой нет разметки, кроме блока div с уникальным идентификатором (id);

  • и есть тег script (здесь в блоке head), в котором подключается React-приложение;

  • затем браузер парсит эту страничку, и когда доходит до тега </script>, загружает файл со скриптом и исполняет его;

  • библиотека ReactDOM встроит приложение в блок div c идентификатором root, и после этого мы увидим наше приложение в браузере.

  • будет сделан API-запрос на получение данных

  • React отобразит данные, полученные в ответе от сервера, на странице

Итого для отображения списка фильмов, React делает 3 запроса (без учета стилей, шрифтов и картинок)

Постарался схематично отобразить этот процесс. 

Такой способ отрисовки называется Client Side Rendering (CSR), когда вся работа по рендерингу приложения выполняется на стороне клиента, в браузере.

Приложения, сделанные с таким подходом, легки в разработке и весят меньше в размере (сравнение размеров с разными подходами в главе «Синтетические тесты»). Когда у вас ограниченные ресурсы на хостинге или ваше приложение закрыто для всех, например у вас панель администрирования, то CSR вам отлично подойдет.

Если мы опубликуем наше приложение в сети и попытаемся найти в Гугле, то мы получим минимум информации о содержимом сайта. Всё дело в том, что SEO анализаторы, сканируя приложение, не загружают JavaScript, и не поймут, какое содержание у нашего сайта. 

А если у вас тот случай, когда сайт публичный и важна настройка SEO-аналитики, то здесь отлично подойдет SSG.

SSG: Static Site Generation

Static Site Generation — подход, когда содержимое сайта генерируется в html-файлы. Для такой генерации на React существует несколько инструментов например Next.js или Gatsby. 

Схематично отобразил работу SSG. 

Как можно заметить на схеме, единственное отличие от CSR, в том что с сервера возвращается сформированная страница. Об этом чуть ниже.

Давайте перепишем наше приложение на Next.js, для установки выполним команду npx create-next-app@latest

Затем в файл /pages/index.js перенесем наш код из create-react-app.

export default function Home() {
  const [isLoading, setIsLoading] = useState(false);
  const [movies, setMovies] = useState([])


  useEffect(() => {
    setIsLoading(true);
    fetch('./db/movies.json')
        .then(response => response.json())
        .then(data => {
            setMovies(data);
            setIsLoading(false);
        })
  }, [])

  return (
    <>
      <Head>
        <title>Create Next App</title>
        <meta name="description" content="Generated by create next app" />
        <meta name="viewport" content="width=device-width, initial-scale=1" />
        <link rel="icon" href="/favicon.ico" />
      </Head>
      <section>
        <h1>Киноафиша</h1>
        <p>
          Киноафиша – это наиболее полная информация о кино и кинотеатрах.
          <br />
          У нас вы найдёте подробные сведения о фильмах, новостях мира кино и кинозвёзд
        </p>
        <div className="movies">
          { isLoading ?
              "Загрузка..."
              : movies.map(movie =>
                  <Card key={movie.title} url={movie.url} title={movie.title} rating={movie.rating} />)
          }
        </div>
      </section>
    </>
  )
}

И в package.json в scripts поменяем build на "build": "next build && next export",, чтобы после билда получить сгенерированный html.

Билд делается командой npm run build и уже в папке out будет лежать index.html.

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charSet="utf-8" />
    <title>Create Next App</title>
    <meta name="description" content="Generated by create next app" />
    <meta name="viewport" content="width=device-width, initial-scale=1" />
    <link rel="icon" href="/favicon.ico" />
    <meta name="next-head-count" content="5" />
    <link rel="preload" href="/_next/static/css/3a8573f30cf016c2.css" as="style" />
    <link rel="stylesheet" href="/_next/static/css/3a8573f30cf016c2.css" data-n-g="" />
    <noscript data-n-css=""></noscript>
    <script defer="" nomodule="" src="/_next/static/chunks/polyfills-c67a75d1b6f99dc8.js"></script>
    <script src="/_next/static/chunks/webpack-8fa1640cc84ba8fe.js" defer=""></script>
    <script src="/_next/static/chunks/framework-2c79e2a64abdb08b.js" defer=""></script>
    <script src="/_next/static/chunks/main-74c4d6b2b5c362f3.js" defer=""></script>
    <script src="/_next/static/chunks/pages/_app-97d725a4bc5ca324.js" defer=""></script>
    <script src="/_next/static/chunks/pages/index-338f46d7fb8e6403.js" defer=""></script>
    <script src="/_next/static/m-SkEnGjpc5wOy7zzkoJm/_buildManifest.js" defer=""></script>
    <script src="/_next/static/m-SkEnGjpc5wOy7zzkoJm/_ssgManifest.js" defer=""></script>
  </head>
  <body>
    <div id="__next">
      <section>
        <h1>Киноафиша</h1>
        <p>Киноафиша – это наиболее полная информация о кино и кинотеатрах. <br />У нас вы найдёте подробные сведения о фильмах, новостях мира кино и кинозвёзд </p>
        <div class="movies"></div>
      </section>
    </div>
    <script id="__NEXT_DATA__" type="application/json">
      {
        "props": {
          "pageProps": {}
        },
        "page": "/",
        "query": {},
        "buildId": "m-SkEnGjpc5wOy7zzkoJm",
        "nextExport": true,
        "autoExport": true,
        "isFallback": false,
        "scriptLoader": []
      }
    </script>
  </body>
</html>

В отличие от CSR, в SSG в body есть содержание с заголовком и описанием, но в div с классом “movies” пусто. Связано это с тем, что в SSG, логика, связанная с API запросами, выполняется на стороне клиента и для решения этой проблемы существует SSR.

Примечание. На самом деле в SSG можно делать запросы, и делаются они во время запуска билда. Когда запускается билд, фреймворк, в нашем случае Next.js, делает запрос на сервер и сохраняет ответ в JSON-файл, и полученный список фильмов будет занесен в html. Данные будут не динамическими — всегда одни и те же, — но такой подход отлично подойдет для сайтов визиток, на которых информация не меняется.

В случае с CSR и SSG данные с сервера грузятся после получения html-страницы клиентом — показывается спинер загрузки и делается запрос данных с сервера. А что если мы хотим получать данные сразу, без отображения индикатора загрузки?  Для этого существует SSR.

SSR: Server Side Rendering

SSR — Server Side Rendering, способ генерации html на стороне сервера.

В CSR и SSG сервер возвращает готовую статическую страницу. В случае серверного рендеринга, после запроса клиентом странички, сервер на своей стороне выполняет API-запросы, а затем формирует html-страницу.

Схематичное отображение работы SSR.

По схеме можно понять, что страница со списком фильмов будет получена за 1 запрос.

Давайте теперь попробуем переделать наше приложение используя SSR. Для этого также будем использовать Next.js – установка как и в случае с SSG через команду npx create-next-app@latest, только в этом случае не будем менять в package.json значение в build.

Чтобы запросы проходили на стороне сервера Next.js предоставляет два метода: getInitialProps и getServerSideProps. Первый метод считается устаревшим и не рекомендуется для использования, и поэтому будем использовать getServerSideProps. Подробно о разнице между этими методами можно почитать в статье «getInitialProps vs. getServerSideProps in Next.js»

Перенесем логику в getServerSideProps.

import Head from 'next/head'
import Card from "@/components/Card";
import process from "next/dist/build/webpack/loaders/resolve-url-loader/lib/postcss";

// указываем сервер, чтобы при локальной разработке не было ошибки при запросе в getServerSideProps
const server = process.env.NODE_ENV === 'production' ? 'https://mysite.com' : 'http://localhost:3000'

export default function Home({ movies }) {
  return (
      <>
        <Head>
          <title>Create Next App</title>
          <meta name="description" content="Generated by create next app" />
          <meta name="viewport" content="width=device-width, initial-scale=1" />
          <link rel="icon" href="/favicon.ico" />
        </Head>
        <section>
          <h1>Киноафиша</h1>
          <p>
            Киноафиша – это наиболее полная информация о кино и кинотеатрах.
            <br />
            У нас вы найдёте подробные сведения о фильмах, новостях мира кино и кинозвёзд
          </p>
          <div className="movies">
            { movies.map(movie =>
                    <Card key={movie.title} url={movie.url} title={movie.title} rating={movie.rating} />)
            }
          </div>
        </section>
      </>
  )
}

export async function getServerSideProps() {
  const response = await fetch(server + '/db/movies.json');
  const data = await response.json();

  return {
      props: {
          movies: data
      }
  }
}

Когда разбирали пример с клиентским рендерингом, говорили о том что клиент получает пустую страничку и браузер исполняет JavaScript-код и рендерит приложение. В случае же с SSR, для рендеринга React-компонент используется Node.js сервер, а для его хостинга потребуются дополнительные ресурсы. Если у вас ограниченные ресурсы на сервере, то вам не сильно подойдет выбор SSR. 

Но если вы решили использовать серверный рендеринг, то стоит здесь также добавить, что дополнительная нагрузка ложится на сервер, за счет того, что асинхронные запросы выполняются на стороне сервера. Для таких случаев существует подход комбинирования запросов: часть выполняют на стороне сервера (как в примере c SSR), а часть на стороне клиента (SSG/CSR).

Синтетические тесты

Перед тем как подвести итоги я обернул приложение в Docker образы и задеплоил на сервер. Итоговый размер приложений составил:

В этом тесте важно отметить, что в CSR и SSG в качестве веб-сервера используется nginx 1.17-alpine, который занимает большую часть размера образа. Размер самого кода приложения – то, что лежит в папке build или out – 565 Кб и 410 Кб, соответственно. 

У SSR же используется Node.js в качестве сервера и генерация статики происходит во время запроса на сервер — здесь итоговый размер занимает Node.js и файлы в папке .next.

Важно отметить, что финальный размер приложений можно оптимизировать, настроив сборщик, конфиг Dockerfile, избавиться от ненужных библиотек и.т.д. Но в рамках текущей статьи не стоит вопрос оптимизации, а лишь ознакомление с данными способами отрисовки веб приложений.

Также можно сравнить показатели во вкладке Network (Сеть). Тестирование будем проводить с допущением, что у пользователя медленный интернет, для этого выберем 3G (низкая скорость).

Client Side Rendering.

Client Side Rendering
Client Side Rendering

Static Site Generation.

Static Site Generation

Server Side Rendering.

Server Side Rendering

Можно заметить разницу в количестве запросов: 

  • У CSR отсутствуют запросы на получение JS-файлов webpack, _buildManifest, framework и прочего — это файлы, которые генерирует Next.js. 

  • У SSG на один запрос больше чем в SSR, это GET запрос movies.json. В серверном рендеринге он выполняется на стороне сервера и во вкладке Network он не отображается. 

Можно на этих скринах отметить, что у SSR лучше показатель «Завершено» — время за которое выполняются все запросы, когда полностью загрузится вся страница. В этом можно убедиться если провести синтетические тесты во вкладке Lighthouse.

Client Side Rendering.

Client Side Rendering

Static Site Generation.

Static Site Generation

Server Side Rendering.

Server Side Rendering

Как можно заметить у SSR лучше практически все показатели, особенно стоит обратить внимание на:

  • First Contentful Paint (сколько времени требуется браузеру для отрисовки первой части контента);

  • Time to Interactive (сколько времени требуется, чтобы пользователь мог взаимодействовать со страницей);

  • Largest Contentful Paint (время за которое основное содержимое страницы становится видимым для пользователей);

  • и Speed Index (измеряет, насколько быстро контент отображается визуально во время загрузки страницы).

У SSR показатели лучше, потому что контент страницы сформирован на стороне сервера и клиентских ресурсов тратится меньше.

На загруженном сервере, где будет работа с большими объемом данных и потоком пользователей показатели у SSR будут хуже, если сам сервер не будет достаточно мощным, чтобы быстро сформировать страницу.

У SSG выше показатель Speed Index, чем у CSR - в первом случае возвращается не пустая страница, а с какой-то разметкой, за счет этого браузер быстрее отобразит контент, во втором же случае идет загрузка пустой страницы, далее загрузка скрипта и затем отображение содержимого приложения. 

Если бы данные на странице были бы постоянными, и мы бы их получили во время билда (в Next.js через getStaticProps) — SSG, то показатели данного подхода были бы такие же как у SSR, за счет того что страница уже сформирована и на клиенте не нужно делать запросы.

В статье не стал приводить тесты на показатели SEO, потому что приложение достаточно простое и все показатели схожие. Но по опыту, могу сказать, что на более сложных проектах у CSR будет хуже SEO.

Итоги

Резюмируя можно сказать, что выбор того или иного инструмента зависит от ваших потребностей и возможностей. Ведь ещё имеет место быть разные погрешности — качество сервера, скорость интернета, мощность устройства клиента. 

  • Если у вас непубличный проект, то лучше подойдет CSR. 

  • Если у вас сайт визитка или такой, в котором данные статичны и нужна настройка SEO, то можно сделать выбор в пользу SSG. 

  • Если же данные меняются постоянно, то стоит также учесть производительность веб-сервера, посещаемость ресурса - есть средства на хороший сервер - можно выбрать SSR.

Немного полезных материалов:

How You Render Can Affect Your SEO (CSR vs SSR vs Dynamic Rendering)

SSR vs CSR- which is the right choice for your Progressive Web App (PWA)?

Javascript SEO: What is SSR/CSR? Advantages and Disadvantages

Gatsby: The Fastest Frontend for the Headless Web

Nuxt - The Intuitive Vue Framework

Server-side rendering (SSR) with Angular Universal


Рекомендуем почитать [подборка от редактора]

Также подписывайтесь на Телеграм-канал Alfa Digital — там мы постим новости, опросы, видео с митапов, краткие выжимки из статей, иногда шутим.

Комментарии (1)


  1. Areso
    00.00.0000 00:00

    Хотел бы отметить, что большой футпринт у SSR не является обязательным, скорее это последствие выбора конкретной технологии.