Главная / Статьи / Браузер, сервер или время сборки: карта принятия решений для архитектур фронтенда

Браузер, сервер или время сборки: карта принятия решений для архитектур фронтенда

Посмотрите, как SSG, SSR, стриминг, серверные компоненты, BFF, обработка на краю, модульные монолиты и микро-фронтенды каждый отвечают на один вопрос: где должна выполняться работа?

2588 слов

В обсуждениях архитектуры, особенно при собеседованиях с высокопоставленными специалистами по фронтенду, редко важно, что именно вы создали. Важно то, почему вы сделали это именно так, и ответ «это то, что уже было у команды» не является приемлемым. Хорошая новость в том, что практически каждый шаблон архитектуры фронтенда отвечает на один и тот же основной вопрос: сколько работы должно выполняться в браузере, сколько — на сервере и сколько — во время сборки? Как только вы поймете, что SSR, серверные компоненты, backends-for-frontends и микрофронтенды — это разные способы проведения этой границы, выбор между ними и обоснование своего выбора становится гораздо проще.

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

Как изменилась граница между клиентом и сервером

Ранний веб был простым. Статические файлы HTML загружались практически мгновенно, и в них было очень мало элементов, которые могли сломаться.

Затем фреймворки MVC с серверной стороны, такие как Django, Rails и ASP.NET, начали генерировать HTML при каждом запросе. Теперь страницы могли отображать динамические данные, но каждое нажатие приводило к полной перезагрузке страницы.

Приложения с одной страницей, созданные с использованием React, Vue или Angular, резко изменили ситуацию в противоположном направлении. Браузер взял на себя управление маршрутизацией, состоянием, проверкой данных и иногда даже аутентификацией. В результате получился интерфейс, который кажется мгновенным, но только после загрузки. Однако есть недостаток: большие пакеты JavaScript, медленная первая отрисовка и худшая доступность контента. Google действительно выполняет JavaScript, но отрисовка на крупных сайтах может замедлить процесс индексации, а многие другие роботы-поисковики, включая ботов для превью в социальных сетях и многих ИИ-роботов, вообще не выполняют скрипты. Контент, который они не могут эффективно увидеть, для них просто не существует.

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

Backend-for-Frontend: переосмысление одного API на клиента

В конфигурации Backend-for-Frontend (BFF) команда фронтенда запускает собственный тонкий сервис перед реальными бэкенд-сервисами. Этот сервис выполняет одну функцию: преобразует данные бэкенда именно в то, что необходимо конкретному клиенту.

Эта паттерн-архитектура обычно связывают с SoundCloud. Примерно в 2013 году, при преобразовании монолитной системы в микросервисы, компания обнаружила, что клиенты для веба, iOS и Android соревнуются за один общий API, который не подходил ни для кого из них. Каждому клиенту был предоставлен собственный легкий бэкенд. Фил Кальсадо, участвовавший в этой миграции, позже подробно описал её историю, а статья Сэма Ньюмана превратила её в широко цитируемое описание паттерна, которое и дало ему название. Netflix пришла к аналогичному решению самостоятельно, используя адаптерные слои для конкретных устройств, поскольку приложения для телевизора и телефона требуют совершенно разных данных.

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

BFF решает эту проблему, предоставляя каждому интерфейсу индивидуальный ответ, адаптированный под его потребности. Приведённый ниже сервис Express, использующий встроенную функцию fetch в Node 18 и новее версиях, вызывает API продукта и возвращает только четыре поля, переименовывая title в name и сокращая список изображений до одной миниатюры:

// bff.js — Node 18+, fetch is built in
import express from 'express';

const app = express();
const API = 'https://api.example.com';

app.get('/products/:id', async (req, res) => {
  const response = await fetch(`${API}/products/${req.params.id}`);
  const data = await response.json();

  res.json({
    id: data.id,
    name: data.title,
    price: data.price,
    thumbnail: data.images[0],
  });
});

app.listen(4000, () => console.log('BFF listening on :4000'));

Клиент запрашивает /products/123 и получает именно тот формат данных, который ему нужен, без лишних полей и дополнительных запросов. В производственном коде также следует проверять значение response.ok перед парсингом, а также защищаться от случаев, когда у продуктов нет изображений, поскольку data.images[0] предполагает наличие хотя бы одного изображения.

Этот подход требует большего внимания, чем обычно уделяется. BFF представляет собой ещё один сервис, который необходимо развернуть, отслеживать и поддерживать в рабочем состоянии. Если он выходит из строя, останавливается и интерфейс, даже если сам фоновый сервер работает без проблем. Это не бесплатная инфраструктура; это дополнительная точка отказа, главной выгодой которой является удобство работы для команды фронтенда. Чтобы узнать больше о создании такого сервиса внутри приложения Next.js, посмотрите как превратить обработчики маршрутов Next.js в специализированный слой BFF.

Стратегии отрисовки: где формируется HTML

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

Статическое генерирование и поэтапная регенерация

Статическое генерирование сайтов (SSG) позволяет отрисовать каждую страницу во время сборки и предоставлять обычные файлы через CDN. Нет ничего быстрее или дешевле для обслуживания, но контент остаётся неизменным до следующей развертки.

Поэтапная статическая регенерация (ISR) предоставляет возможность автоматического обновления: страница может пересоздаваться на фоне через заданный интервал. Таким образом сохраняется большая часть скорости работы статических файлов без необходимости повторной развертки при каждом изменении контента.

Отрисовка на стороне сервера и стриминг

Обработка на стороне сервера (SSR) генерирует HTML для каждого запроса. В своей классической форме сервер отправляет полную страницу, после чего браузер дополняет её: он загружает пакет JavaScript и привязывает обработчики событий и состояние к маркапу, уже отображаемому на экране.

В React 18 была введена технология потокового SSR с помощью renderToPipeableStream, которая отправляет фрагменты HTML сразу, как только готова каждая часть структуры, вместо того чтобы ждать самого медленного компонента. Потоковая обработка является стандартным выбором для новых приложений, хотя многие продакшен-приложения по-прежнему без проблем используют классический способ обработки SSR одновременно для всей страницы.

Компоненты сервера, острова и возможность возобновления работы

React Server Components (RSC) и архитектура островов идут еще дальше: JavaScript передается только тем интерактивным частям страницы, которые действительно нуждаются в нем. Статический контент остается обычным HTML без необходимости в процессе гидратации. механизм островов в Astro применяет эту идею вне контекста React. Qwik идет еще дальше благодаря возможности возобновления работы, которая в значительной степени исключает необходимость гидратации путем сериализации состояния приложения в HTML, что позволяет клиенту продолжить работу с того момента, на котором остановился сервер.

В приведённом ниже примере показана страница с серверным компонентом в Next.js App Router. Начиная с версии Next.js 15, params представляет собой Promise, который необходимо дождаться, поэтому компонент разбирает id только после выполнения операции await params. Загрузка данных происходит на сервере, поэтому название продукта и цена поступают в виде уже готового к отображению HTML:

// app/products/[id]/page.tsx (Next.js 15+)
import { fetchProduct } from '@/lib/api';

export default async function ProductPage({
  params,
}: {
  params: Promise<{ id: string }>;
}) {
  const { id } = await params;
  const product = await fetchProduct(id); // runs on the server

  return (
    <main>
      <h1>{product.name}</h1>
      <p>${product.price}</p>
      <AddToCartButton productId={product.id} />
    </main>
  );
}

Только AddToCartButton содержит клиентскую JavaScript-логику. Чтобы это было возможно, она должна находиться в отдельном файле, помеченном директивой 'use client', и импортироваться в страницу; ради краткости в примере этот импорт опущен. В результате получается небольшой пакет, который интерактивен там, где это важно, а в остальных местах остаётся статическим.

Отрисовка на сервере и причины частичного изменения подхода индустрией

Рендеринг на краю сети является одним из самых наглядных примеров в современном фронтенде того, как идеи тестируются на публике и постоянно совершенствуются.

Примерно с 2021 по 2023 год аргументы в пользу этого подхода казались убедительными: необходимо выполнять SSR на краю сети, на таких платформах, как Cloudflare Workers или Vercel Edge Functions, в центре обработки данных, расположенном рядом с каждым пользователем. Меньшее расстояние — более быстрая загрузка страниц. Vercel активно продвигал этот подход.

Позже Vercel открыто изменил свою позицию; тогдашний вице-президент по продукту описал ситуацию как «этот случай меня обманул». Причина этого весьма показательна. Вычисления должны находиться рядом с пользователем, но в то же время — рядом с данными, причем большинство баз данных расположены в одном регионе. Функция на краевом сервере в Токио, выполняющая несколько запросов к базе данных в Вирджинии, часто работает медленнее, чем простая обработка данных непосредственно в Вирджинии. Когда Vercel тестировал это на собственном продукте v0, обычная обработка с использованием Node.js оказалась эффективнее, чем обработка через краевые функции. Впоследствии Vercel отказался от использования автономных краевых функций и теперь рекомендует использовать среду выполнения Node.js, при этом вычисления должны производиться в том же регионе, что и данные; для получения точной информации о статусе каждой среды выполнения обратитесь к документации платформы.

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

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

Модульный монолит фронтенда

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

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

  • Слой платформы, находящийся в ведении команды по разработке платформы, содержащий систему дизайна, общие компоненты, механизмы логирования и другую инфраструктуру.
  • Слой домена, состоящий из папок с функциями, таких как user/ или payments/, каждая из которых находится в ведении отдельной команды.

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

Микро-фронтенды: независимость с издержками

Архитектура микро-фронтендов рассматривает каждую область как отдельное приложение, которое можно развернуть самостоятельно; она обычно загружается во время выполнения основного приложения с помощью механизма вроде Webpack Module Federation.

Вы получаете настоящую автономию: команды могут выпускать продукты по собственному графику и, если это действительно необходимо, использовать даже разные фреймворки. Zalando, IKEA и DAZN описали опыт внедрения такого подхода в крупном масштабе, отметив наличие крупных инженерных команд и значительные инвестиции в совместные инструменты. micro-frontends.org по-прежнему остается стандартным источником информации по этой теме.

Способы сбоев, которые действительно наносят ущерб

Проблема, которая постоянно встречается в отчетах о реальных инцидентах, заключается не в смешивании фреймворков. Это изменение общих зависимостей. Одно удаленное приложение обновляет общую библиотеку, в то время как другое — нет, и вдруг на одной странице начинают работать две копии React, конкурирующие за один и тот же DOM. Module Federation позволяет объявлять общие синглтоны и диапазоны версий для предотвращения этого, но только если команды соглашаются с этими ограничениями и соблюдают их. Именно эта проблема координации разрушает команды гораздо сильнее, чем обычно упоминаемая «сложность».

Существует также пример предостережения в противоположном направлении. По сообщениям, Spotify несколько лет назад экспериментировала с подходом микро-фронтенда на основе iframe в своем десктопном клиенте, а позже объединила его в единую архитектуру, отчасти потому, что разделение компонентов обходилось дороже, чем приносила гарантированная независимость. Даже в крупном масштабе такой подход не гарантирует автоматического успеха.

Пусть размер команды определяет решение

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

  • в таких случаях почти всегда выгоден модульный монолит. Недостаточно людей, чтобы оправдать наличие отдельных процессов развертывания.
  • Примерно от 15 до 50 инженеров с четко определенными областями ответственности: BFF в сочетании с хорошо организованным модульным монолитом обычно достаточно. Микро-фронтенды, вероятно, еще слишком рано.
  • Более 50 инженеров, при которых команды действительно мешают друг другу выпускать обновления: в этом случае микро-фронтенды начинают приносить пользу, но не из-за роста приложения, а из-за развития организации.
  • Убедительное обоснование архитектурного выбора

    На высоких должностях ожидается принятие решения, а не перечень вариантов. Качественный ответ обычно состоит из четырех частей:

    1. Что вы используете. Например: страницы маркетинга генерируются статически, панели управления работают по принципу SSR, а BFF располагается перед мобильным приложением.
  • Почему. Статическая генерация обеспечивает очень быстрое отображение контента, который редко меняется; SSR позволяет выводить персонализированные данные, такие как имя пользователя, без появления неверного контента.
  • Цена, которую вы решили заплатить. BFF добавляет дополнительный этап обработки, но предоставляет команде фронтенда контроль над своим контрактом данных, что оправдывает его использование.
  • Что вы отклонили и почему. Были рассмотрены микро-фронтенды, но из-за дополнительных затрат на координацию они не оправдали себя при текущем размере команды.
  • Последний пункт имеет наибольшее значение. Объяснение того, что вы намеренно не выбрали, — вот что отличает простое перечисление фактов от принятия решения. Пример изменения подхода к отрисовке на периферии — это готовая иллюстрация: даже платформа, выступавшая за этот подход, изменила стратегию, когда показатели не совпали.

    Часто задаваемые вопросы

    В чём разница между SSR и SSG?

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

    Стоит ли использовать BFF, если есть только один фронтенд?

    Как правило, нет. BFF оправдан тогда, когда несколько клиентов — например, мобильные устройства, веб-приложения и API партнёра — нуждаются в существенно разных данных от одного бэкенда. При наличии только одного фронтенда он в основном добавляет дополнительный этап передачи данных по сети и ещё один сервис для работы.

    Можно ли считать технологию edge rendering устаревшей?

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

    Когда команде стоит перейти на микро-фронтенды?

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

    Основные выводы

    • Каждый из описанных подходов — это ответ на один вопрос: какая часть работы относится к браузеру, серверу и этапу сборки.
  • Правильный подход зависит от размера команды, местоположения данных и сложности процессов развертывания гораздо больше, чем от того, какой шаблон кажется наиболее впечатляющим.
  • FFF, микро-фронтенды и обработка на краю все они идут на увеличение операционных затрат ради определенной выгоды; четко укажите этот компромисс.
  • Имейте под рукой обоснование отклоненных вариантов. Это самое убедительное доказательство того, что выбор был сделан намеренно.