Каждый год веб совершает огромные шаги в светлый мир будущего (или тёмный, смотря какой вы предпочитаете). Инструменты один за другим добавляют тёмные темы, а крупные гиганты обновляют и улучшают свои системы дизайна, чтобы они оставались актуальны в расширяющемся тёмном мире. Внедрение темной темы значительно улучшает пользовательский опыт и, как следствие, бизнес показатели. Например, недавно одна из крупнейших бразильских новостных компаний Terra, после добавления темной темы, увеличила количество посещённых за сеанс страниц на 170% и снизила показатель отказов на 60% (т.е. в 2,5 раза) [читать статью].

По собранным Android Authority (2514 опрошенных) данным и анализу Томаса Стейнера (243 опрошенных), более 80% пользователей используют тёмную тему. Конечно же, выборку сложно назвать однозначно правдивой, ведь опросы проходили на технических форумах, но в целом можно говорить о том, что темной темой пользуется добрая половина интернета.

Первая часть цикла по большей части была посвящена истории css-переменных – их созданию, развитию и становлению, а также содержала примеры темизации как на уровне планирования и дизайна, так и на уровне разработки клиентской части, включая различные способы темизации и смены тем [Темизация. История, причины, реализация]. В этой статье, поднимаясь на ступень выше, речь пойдёт о клиент-серверном взаимодействии и возможностях современных браузеров в контексте темизации.

Серверный рендеринг. SSR.

Прежде чем погружаться вглубь темизации на сервере стоит ненадолго приостановиться на теме серверного рендеринга – что это и с чем это едят.

История серверного рендеринга началась с создания PHP Расмусом Лердорфом, в 1994 году, всего через год после создания html Сэром Тимоти Джон Бернерс-Ли (также создатель URI, URL, HTTP, HTML и Всемирной паутины [совместно с Робертом Кайо]). Несмотря на то, что PHP вплоть до 2014 года развивался без полноценной спецификации, его популярность в те годы была чрезвычайно высокой. В 2003 году эту волну популярности сильно подтолкнул WordPress, на котором по сей день стоит 40% интернета.

Помимо PHP занять нишу языка для серверного рендеринга предпринимались и другими языками. Например, java с использованием servlet, или Ruby и его фреймворк для разработки веб приложений Ruby on Rails. Но сколь либо значимого веса на рынке они добиться так и не сумели.

Смутные времена единовластия PHP немного приблизились к закату в 2009 году, с появлением node.js, а точнее в 2010 году, когда Тиджей Головайчук написал express.js.

Следующая веха начала формироваться с 2010 до 2014 года, во время формирования большой тройки – Angular (2010г.), React (2013г.), Vue (2014г.), заложившей крепкий фундамент новым типам веб-приложений – SPA (single page application/одностраничные приложения).

Все они отличались общей проблемой – отсутствие какой-либо SEO оптимизации. Поэтому впоследствии для них была создана не менее крупная тройка фреймворков – Next.js (2014г.), Nuxt.js (2016г.), NestJS (2017г.). Они позволяли генерировать приложение на сервере, тем самым давая поисковым роботам готовый контент.

Также SSR выгодно отличается от SPA тем, что:

  • Поддерживается open graph, тем самым дополнительно улучшая SEO и добавляя предпросмотр в социальный сетях

  • Для большинства приложений значительно улучшается FCP (first contentful paint) в связи с тем, что в ответе с сервера приходит сразу готовая страница

  • Пользователи с отключенным javascript получат полноценную страницу, а пользователи с медленным интернетом/слабым устройством с большей вероятностью увидят контент в первые 3 секунды.

Серверный рендеринг прошёл большой путь становления, ровно также как и другие варианты рендеринга сайта.

Альтернативы SSR

Одностраничное приложение (SPA) – та самая «великая тройка». Сайт генерируется в js файлы, а отрисовка всего контента происходит на клиенте.

Статический сайт. Сайт генерируется в статические страницы и впоследствии с сервера раздаются эти файлы. Клиент сразу получает готовую страницу.

Также популярен и гибрид этих двух подходов, который присутствует в упомянутых ранее Next.js и Nuxt.js из коробки – статическая генерация сайта (SSG). При таком подходе с сервера раздаётся статичный html, а на клиенте перестраивается виртуальный DOM для того, чтобы впоследствии приложение работало в режиме SPA.

Разница в контексте темизации

Так как в SPA отрисовка происходит на клиенте – определение темы и добавление стилей для неё должны находиться на верхнем уровне приложения. Соответственно вся логика, связанная с настройкой тем должна быть вынесена в отдельный bundle или же рендеринг должен происходить только после определения темы. Дополнительные сложности могут возникнуть в проектах с единственным источником правды, так как через него должны проходить все действия или в проектах со стилями, хранящимися в глобальных объектах (напр. Css-in-js позволяет использовать этот объект внутри функций стилей).

В статическом сайте логика темизации также должна быть вынесена на верхний уровень, но им (в отличии от SPA) после этого не нужно рендерить всю страницу. В обоих случаях короткий промежуток определения темы пользователя будет сопровождаться скачком цвета (в случае несовпадения темы пользователя с темой приложения по умолчанию).

SSR же позволяет отрисовать на сервере сразу нужную тему (в зависимости от сохранённой в файлах cookie темы), а с недавних пор это распространяется даже на новых пользователей.

Google. определение темы по заголовку

Раньше единственным способом определить тему пользователя было добавление клиентского скрипта. В августе теперь уже прошлого года, с релизом 93 версии, google добавили поддержку заголовков, которые позволяют передавать тему устройства пользователя на сервер. (функция на самом деле отлично работала и в 92 версии). Функционал основан на клиентских подсказках (сухая документация стандарта – https://datatracker.ietf.org/doc/html/rfc8942).

Они позволяют серверу запросить нужные данные о пользователе. Эти данные будут добавлены в заголовки запроса.

В случае темы устройства в ответ сервера должны быть добавлены следующие заголовки:

Accept-CH: Sec-CH-Prefers-Color-Scheme
Sec-CH-Prefers-Contrast Vary: Sec-CH-Prefers-Color-Scheme
Critical-CH: Sec-CH-Prefers-Color-Scheme

В запрос добавится следующий заголовок:

Sec-CH-Prefers-Color-Scheme: "dark"

В общем-то именно после полноценного внедрения этого api Google наконец-то добавили тёмную тему в поиск.

К сожалению, функционал подсказок поддерживается только браузерами на ядре Chromium. Все остальные браузеры (в числе которых safari и firefox) подсказки не поддерживают.

Caniuse. Поддержка клиентских подсказок
Caniuse. Поддержка клиентских подсказок

Реализация темизация на сервере

Принципы описания классов и стилей, а также клиентская логика описаны в предыдущей статье.

Дальнейшие примеры будут построены на фреймворке next.js. Такую же логику можно повторить на любом другом фреймворке.

Выбранную на клиенте тему сохраняем в файлах cookie.

const changeTheme = (newTheme: Theme) => {
    document.cookie = `theme=${newTheme};path=/;max-age=31536000`;
    // ...
};

Сперва проверяем, сохранена ли у пользователя тема.

const cookieTheme = ctx.req.cookies.theme;

Если тема пользователя не сохранена, то определяем тему пользователя по заголовку.

const userDeviceTheme = ctx.req.headers['sec-ch-prefers-color-scheme'] as string;

Если тема не сохранена или сохранена не валидная тема, то возвращаем тему по умолчанию.

const userDetectedTheme = cookieTheme || userDeviceTheme;
const defaultTheme = 'light';
const theme = (userDetectedTheme === 'light' || userDetectedTheme === 'dark') ? userDetectedTheme : defaultTheme;

Передаём тему пользователя на клиентскую часть и используем при рендере.

export const getServerSideProps: GetServerSideProps = async (ctx) => {
  const userDeviceTheme = ctx.req.headers['sec-ch-prefers-color-scheme'] as string;
  const cookieTheme = ctx.req.cookies.theme;
  const userDetectedTheme = cookieTheme || userDeviceTheme;
  const defaultTheme = 'light';
  const theme = (userDetectedTheme === 'light' || userDetectedTheme === 'dark') ? userDetectedTheme : defaultTheme;
  return ({
    props: {
      theme,
    },
  });
};

Функция getServerSideProps отрабатывает на сервере и передаёт возвращаемое значение странице в качестве props [подробнее].

Что же выбрать – SPA, SSR или SSG.

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

Между SSG и SSR выбор зависит от следующих параметров:

  • Является ли ваш контент статичным (один раз собрали, и больше он не меняется)

  • Зависит ли контент от конкретного пользователя (расположения, агента и др.)

  • Нужно ли выполнять ряд серверных операций до отрисовки (напр. обращения к БД)

В случае темизации подходит второй пункт – в зависимости от темы пользователя отрисовываются разные страницы. Справедливости ради одного лишь этого пункта недостаточно для перехода на SSR. Все зависит от возможностей сервера, его стабильности и прочих преследуемых целей.

Также SSG и SSR позволяют браузерам корректно индексировать страницу. Несмотря на то, что Google уже умеет индексировать SPA, назвать одностраничное приложение SEO-дружелюбным ещё нельзя.

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

Стандартом проверки производительности в современном мире является Web vitals. Поэтому сравним выдаваемые им показатели для каждого из режимов.

В дальнейших примерах будут показаны 3 варианта рендеринга страниц:

  • SSR на next.js

  • SSG на next.js (примечание, это не статический сайт в классическом его понимании)

  • SPA на react.js

Все варианты идентичны за исключением особенностей библиотек (разный id у корневого элемента, разный подход ко встраиванию в head и различные дополнительные возможности).

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

Сперва посмотрим на сами метрики

  • При совпадении темы по умолчанию с темой пользователя

SPA при совпадении темы
SPA при совпадении темы
SSG при совпадении темы
SSG при совпадении темы
SSR при совпадении темы
SSR при совпадении темы
  • При несовпадении темы по умолчанию с темой пользователя

SPA при несовпадении темы
SPA при несовпадении темы
SSG при несовпадении темы
SSG при несовпадении темы
SSR при несовпадении темы
SSR при несовпадении темы

Теперь о причинах такой разницы

На самом деле отчёты по SSR и SPA при совпадении и несовпадении тем должны быть идентичны.

SPA полностью отрисовывается на клиенте. Соответственно он сперва определяет тему (доли секунды) и только после этого начинает рендерить всю клиентскую часть с нужной темой.

SSG рендерит страницу на сервере, затем на клиенте выстраивается виртуальный дом и сравнивается с реальным. Если изменений нет, то ничего не происходит. Если изменения есть, то происходит ре-рендер клиентской части.

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

К сожалению, эмулятор, используемый в pagespeed insights не содержит никакой информации о теме устройства. Если бы он содержал эти данные, страница пришла бы сразу в светлой теме. Вы сможете проверить и сравнить результаты самостоятельно, все ссылки будут представлены в конце статьи.

Более наглядно это можно посмотреть на потоках

  • При совпадении темы по умолчанию с темой пользователя

Поток SPA при совпадении темы
Поток SPA при совпадении темы
Поток SSG при совпадении темы
Поток SSG при совпадении темы
Поток SSR при совпадении темы
Поток SSR при совпадении темы
  • При несовпадении темы по умолчанию с темой пользователя

Поток SPA при несовпадении темы
Поток SPA при несовпадении темы
Поток SSG при несовпадении темы
Поток SSG при несовпадении темы
Поток SSR при несовпадении темы
Поток SSR при несовпадении темы

Заключение

Здесь должен быть вывод, какой именно вариант выбрать, но давать ответ на этот вопрос было бы слишком самонадеянным. Лабораторные опыты могут показать, какой вариант быстрее или какой будет комфортнее для пользователя (в первую очередь за счёт отсутствия мерцаний при смене темы), но как именно приложение поведёт себя в отдельных проектах предсказать нельзя. Однозначно можно сказать лишь то, что если вы ещё не думали о темизации и у вас развивающийся веб-сервис, то будет полезно уделить внимание этой теме.

Посмотреть весь код можно в github репозитории - https://github.com/vordgi/theming

Список всех вариантов, используемых в сравнении:

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


  1. MaryRabinovich
    12.01.2022 20:07

    Вот кстати да, про тёмную тему. Да.

    Есть ли она на хабре?

    (конечно, "использую её везде, где она есть" - проголосовала)


    1. Vordgi Автор
      12.01.2022 20:28

      Нет, к сожалению, ночного режима на хабре нет.
      Но хаброжителей это не останавливает, и можно найти расширенные копии хабра. Одна из лучших реализаций описана в статьях Владислава Якушева:
      первая часть и вторая.
      "Мне 17 лет и я уже несколько месяцев делаю клон мобильного приложения Хабра, назвав его соответствующе, модно, со стилем и пафосной точкой в конце — habra. Получилось реализовать несколько фич, которых пока нет ни в официальном приложении из плей маркета, ни на самом сайте."
      Одной из фич является как раз темизация. Возможно вам будет интересно.

      P.S сам не пользуюсь, наверно слишком привык к интерфейсу оригинальной версии


    1. ris58h
      13.01.2022 00:23

      На хабре нет, но есть DarkReader для большинства популярных браузеров.


  1. niyaho8778
    12.01.2022 21:41

    я правильно понимаю что серверный рендеринг работает только 1 раз для первой загружер1 страницы, а далее всё через клие


    1. Vordgi Автор
      12.01.2022 21:45

      Судя по вашему комментарию ("1 раз для первой загруженной страницы, а далее всё через клиента" - это может означать только SPA на клиенте) вы подразумеваете next / nuxt / sapper.
      В таком случае ответ и да и нет.
      Да – полноценный рендер в html происходит только для первой страницы, это верно. Вместе с html придёт весь клиентский код для этой страницы и на клиенте создастся виртуальный дом, в дальнейшем все изменения на этой странице будут происходить на клиенте. Исключением могут быть ленивые компоненты.
      Нет – как написал выше - клиентский код приходит для текущей страницы. При переходе* на другую страницу с сервера будет получен объект, описывающий следующую страницу и также будет содержать полученные для этой страницы props-ы (например после обращения к БД или из файловой системы). Ленивые компоненты также загружаются по мере необходимости.

      * Обычно (для этого есть дополнительная опция в api инструментов) это работает не при переходе на другую страницу, а при наведении на ссылку.


  1. copperfox777
    13.01.2022 10:35
    +1

    Автор сравнивал скорость spa и ssg. Так вот на примере сложнее чем туду лист, ssg существенно выигрывает в скорости. Хотя бы потому что прилетает готовый html.


    1. Vordgi Автор
      13.01.2022 12:38

      Отчасти вы правы, поэтому перед метриками стоит дисклеймер: Метрики приведённые ниже никак не отражают действительности в отношении производительности конкретных вариантов. Примеры приведены только для того, чтобы показать относительную скорость и пользовательский опыт от взаимодействия с сайтом в контексте темизации.

      А в контексте приведенных примеров дела обстоят примерно так. (Буду использовать условные единицы для обозначения трудозатрат)
      При совпадении темы в SPA:
      Загрузка приложения (10у.е.) -> определение темы на клиенте (1у.е.) -> рендер приложения (8у.е.) = 19у.е.
      При несовпадении темы в SPA:
      Загрузка приложения (10у.е.) -> определение темы на клиенте (1у.е.) -> рендер приложения (8у.е.) = 19у.е.

      При совпадении темы в SSG:
      Загрузка приложения (14у.е.) [мы грузим дополнительно весь html] -> определение темы на клиенте (1у.е.) -> сравнение реального и виртуального dom (2у.е.) [все совпадает, ничего дальше не происходит] = 17у.е.
      При несовпадении темы в SPA:
      Загрузка приложения (14у.е.) [мы грузим дополнительно весь html] -> определение темы на клиенте (1у.е.) -> сравнение реального и виртуального dom (2у.е.) -> ререндер клиентской части приложения (4у.е.) = 21у.е.

      Повторюсь, это работает так только для конкретного примера - все зависит от проекта. И даже сложные проекты могут повторять ситуацию описанную выше (когда после определения клиентских данных приложение должно быть перерендерено).

      А вообще, в идеале, тема должна быть полностью абстрагирована от кода, но все-таки обычно это не так.