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

Терминология, которую я буду использовать

Визуализация

  • SSR: рендеринг на стороне сервера — рендеринг клиентского или универсального приложения в HTML на сервере.
  • CSR: рендеринг на стороне клиента — рендеринг приложения в браузере, обычно с использованием модели DOM.
  • Регидратация: «загрузка» представлений JavaScript на клиенте таким образом, чтобы они повторно использовали дерево и данные DOM HTML, отображаемые на сервере.

Производительность

  • FP: First Paint — первый раз, когда какой-либо пиксель становится видимым для пользователя.
  • FCP: первая отрисовка контента — время, когда запрошенный контент (текст статьи и т. д.) становится видимым.
  • TTI: время до интерактивности — время, когда страница становится интерактивной (события подключаются и т. д.).
  • TTFB:время до первого байта — время между вводом ссылки и появлением первого фрагмента контента.

Начнем с CSR. В таком процессе рендеринга у нас есть почти пустой html и встроенный Javascript. Мы создаем и строим Дом, загружая этот JS-код в браузер.

КСО — Структура.

Предположим, у нас есть html с пустым тегом div и JS на сервере. Рендеринг CSR выглядит так:

HTML состоит только из одного корневого тега div, а все остальные вещи, такие как отображение и обновление DOM, обрабатываются JS.

Пакеты и производительность

Если мы подумаем о производительности, то сначала увидим, что по мере увеличения сложности страницы будет увеличиваться сложность и размер JS, а также увеличиваться время получения его с сервера. Это вызовет задержку для FCP и TTI страницы.

За и против

Плюсы

  • CSR предоставляет способ иметь SPA, который поддерживает навигацию по страницам без обновления. Это было большой проблемой в течение долгого времени, и теперь веб-сайты могут начать действовать как динамические приложения.

Минусы

Однако есть несколько подводных камней:

  • Производительность. По мере увеличения сложности страницы нам приходится больше ждать, пока JS увидит FCP.
  • Со. Как и посетители, боты отправляют запросы на просмотр страницы. Это хорошо известный процесс как сканирование. На первом этапе роботы Google просматривают исходный код вашей страницы и индексируют весь видимый (не javascript) контент. Но у нас ничего не видно, пока не будет выполнен JS для сборки dom. Во время этой фазы рендеринга он придет снова (иногда это время уходит на месяцы), и теперь он получит реальный контент вашей страницы, доступный для сканирования, и, в конечном итоге, добавит его в индекс.

Поскольку проблемы с производительностью вызваны размером пакета Javascript, существует несколько подходов, повышающих производительность для CSR.

  • Отложенная загрузка. С помощью отложенной загрузки мы можем избежать загрузки некритических ресурсов. Такой подход сократит время начальной загрузки страницы.
  • Агрессивное разделение кода. Это помогает нам создавать несколько пакетов, которые можно динамически загружать во время выполнения. Это также позволяет нам лениво загружать ресурсы JS. Разделение кода поддерживается такими сборщиками, как Turbopack или Webpack.
  • Кэширование с сервисными работниками. Его можно использовать для кэширования оболочки приложения в автономном режиме и увеличения времени работы.

Рендеринг на стороне сервера

Давайте посмотрим, что такое SSR. Это самый старый метод создания полного HTML-кода содержимого страницы, который будет отображаться во время запроса пользователя. Для этой реализации нам нужно создать HTML-код на стороне сервера и отправить его в браузер, чтобы браузер мог легко создать FCP.

Браузер делает запрос на сервер и получает html не только пустой корень, но и сгенерированный с содержимым в нем. Таким образом, браузер показывает контент, как только это возможно. Теперь браузер начинает скачивать ваш бандл, пользователь все еще видит контент на экране, но он еще не интерактивен. Только после того, как ваш пакет загружен, он прикрепляет прослушиватели событий к вашему html и становится интерактивным. Этот процесс мы называем гидратацией или регидратацией.

ССР — плюсы и минусы

Плюсы

Меньше Javascript позволяет браузеру быстрее использовать FCP и TTI.

Поскольку мы избегаем отправки большого количества Javascript на клиентский FCP, а пользователи просто ждут TTI. Когда у нас много элементов пользовательского интерфейса на странице, у SSR гораздо меньше JS, чем у CSR, поэтому время на получение скриптов меньше, даже иногда у нас есть FP=FCP=TTI.

SEO

Боты поисковых систем легко получают контент на первом этапе, поэтому теперь они могут индексировать страницу. Итак, теперь мы получаем довольно хорошее SEO на странице.

Минусы

Иногда у нас может быть медленный TTFB. Это был бы сценарий, когда у пользователя медленная сеть или код сервера не оптимизирован.

Поскольку весь наш код недоступен на стороне клиента, частые обращения к серверу вызывают полную перезагрузку страницы, что иногда увеличивает время между взаимодействиями. Таким образом, SPA невозможно с SSR.

Давайте создадим простое приложение React SSR с Express.

npx create-react-app ssr-react

Удалите лишние файлы в папке src и просто оставьте App.js и index.js.

Напишем компонент: счетчик с методами увеличения и уменьшения.

import React, { useState } from "react";

const App = ({ initialValue }) => {
  const [count, setCount] = useState(initialValue);
  return (
    <>
      <div className="App">
        <button onClick={() => setCount(count + 1)}>Increase</button>
        <div style={{ margin: "20px" }}>{count}</div>
        <button onClick={() => setCount(count - 1)}>Decrease</button>
      </div>
    </>
  );
};


export default App;

Теперь мы хотим отрендерить его на стороне сервера. Сначала нам нужен сервер, поэтому создайте новую папку server и добавьте в нее файл Server.js. Я буду использовать экспресс для создания сервера, давайте сначала установим его.

npm i express --save

Создайте экспресс-сервер и слушайте его на порту 3000.

import express from "express";
const app = express();
app.listen(3000, function () {
  console.log("server running on 3000");
});

Нет, у нас есть сервер. Что нам нужно, так это получить построенный html-файл. Мы будем отображать компонент приложения и передавать его в ответ. Нам также нужно обслуживать статические файлы.

import express from "express";
import fs from "fs";
import path from "path";
import App from "../src/App";
import React from "react";
import ReactDOMServer from "react-dom/server";
const app = express();

app.get("/page", async (req, res) => {
  fs.readFile(path.resolve("./build/index.html"), "utf-8", (err, data) => {
    if (err) {
      return res.status(500).send("Error happened");
    }
    const html = `<div id="root">${ReactDOMServer.renderToString(
      <App />
    )}</div>`;
    return res.send(data.replace('<div id="root"></div>', html));
  });
});

app.use(express.static(path.resolve(__dirname, "..", "build")));

app.listen(3000, function () {
  console.log("server running on 3000");
});

Итак, мы читаем файл index.html из созданной папки. Затем нам нужно визуализировать компонент приложения на стороне сервера, для этого мы используем ReactDOMServer, который предоставляет метод для визуализации компонента на стороне сервера. Замените пустой корень div нашим визуализированным компонентом, чтобы отправить его клиенту.

Итак, мы используем JSX на стороне сервера и нуждаемся в его поддержке, поэтому мы добавим для него Babel.

npm i @babel/preset-env @babel/preset-react @babel/register ignore-styles --save-dev

добавьте файл index.js рядом с server.js и потребуйте в него эти вещи и потребуйте server.js внизу них.

require("ignore-styles");

require("@babel/register")({
  ignore: [/(node_module)/],
  presets: ["@babel/preset-env", "@babel/preset-react"],
});

require("./server");

На стороне внешнего интерфейса нам больше не нужен ReactDOM.render, так как мы уже отрендерили DOM с сервера, и теперь нам нужно его гидратировать, чтобы зарегистрировать прослушиватели событий и т. д. Теперь index.js в папке src выглядит так.

import React, { StrictMode } from "react";
import { hydrateRoot } from "react-dom/client";
import App from "./App";

hydrateRoot(document.getElementById("root"), <App initialValue={10} />);

добавить последнюю команду запуска в файл package.json

    "ssr": "node server/index.js",

Сначала создайте наш интерфейс с помощью команды npm run build, а затем запустите наш сервер с помощью команды npm run ssr.

Вот и все.

Исходный код Github здесь.