Привет, Хабр! Меня зовут Максим Вишневский, я архитектор (в основном фронтенда) в Mindbox и автор небольшого Telegram-канала Вишнёвые истории. Занимаюсь фронтендом уже более восьми лет, до этого успел пописать бэкенд на Python и C# (не то чтобы сейчас забросил), поадминить разного рода сети и побыть дизайнером. За время своей работы я пришёл к выводу, что переход или попытки понимания фронтенда бэкендерами, особенно когда речь идёт о техлидах, требуют смены мышления и добавления новых категорий в изучение инженерных практик.

Эта статья основана на моём докладе с TeamLead Conf++ 2024. Сегодня я проведу для вас экскурсию в мир фронтенда и немного поделюсь болями: расскажу, из чего он состоит, как работает, обсудим архитектурные проблемы и почему формочку так долго поставлять в продакшен. А также обсудим:

  • технологический ландшафт и экосистему;

  • зачем нужен FrontOps и почему часто возникают споры тонкий/толстый клиент;

  • почему фронтендеры так часто меняют архитектуру — поговорим о ней глобально. Например, зачем мы выдумали микрофронтенды;

  • немного затронем BFF.

Проблематика: как появляются техлиды

Довольно часто у нас есть примерно такой дизайн команды: тимлид, который управляет всем зоопарком, немного занимается технологиями и пытается делать так, чтобы всё работало (процессы, People Management и т.д.), какое-то количество разработчиков, тестировщик (не QA), периодически дизайнеры и аналитики.

Но в какой-то момент команда начинает расти: сначала у нас Scrum, несколько разработчиков. Затем — бац! — их уже 20, и всем этим тоже нужно как-то управлять с точки зрения технологий. В такой момент в команде довольно часто появляется неявный лидер — это почти всегда бэкендер, и его называют техлидом.

Почему именно он:

  • Глубокие знания домена. Бэкендер прекрасно понимает принцип работы бизнеса в этой сфере, погружён в процесс и может его объяснить.

  • Понимание своего техстека. Но только своего: бэкендер он отличный, а о фронтенде поговорим позже.

  • Классно умеет масштабировать, но опять-таки — только бэкенд.

  • Стабильная поставка изменений, не ломающих продакшн. По факту это уникальный скилл: не ломать продакшн сложно, особенно когда производишь глобальные изменения.

  • Видение будущего техландшафта. Бэкенд обладает некоторым визионерством в том, как будет формироваться техландшафт, куда мы все пойдём.

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

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

И получается, что когда тимлид или менеджер приходит к фронтендерам с запросом «Нам формочка нужна», они выкатывают два спринта. Потом тимлид бежит к техлиду, тот — обратно, и начинается: «У нас FSD», «Надо в микрофронт вынести», «Мы с прошлого раза данные с клиента не утащили» и т.д.

Наконец, техлид идёт к другу-фронтендеру с просьбой научить-показать, и начинается вторая стадия отчаяния. В итоге техлид, как этот гусь, возвращается к фронтендерам и говорит: «Это вы всё сделали сложным, как-нибудь поправьте!».

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

Технологический ландшафт и экосистема вокруг

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

Первый — это роутинг. Обычно есть Nginx — мы там всё настроили и пошли в сервисы. Но некоторые до сих пор поддерживают SPA вместе со всем зоопарком. Его нужно знать, так как может возникать параметризированный или, как его ещё называют, Rich-роутинг. Это сложно тестировать и поддерживать. Желательно знать, что ваши фронтендеры с  этим  делают и как формируются запросы. А зоопарк там может быть большой. Советую посмотреть это видео про работу роутинга в SPA, там всё понятно описано.

Теперь переходим на страницу, откуда надо вытащить данные. Вариантов масса: это может быть GraphQL, httpApi, с сокетами можем общаться. Но возникает вопрос проектирования — как этот слой отделить?

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

Передача данных в слой представления (компоненты)

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

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

Через время разработчики устали и начала искать обходные пути. Одним из первых подходов стало использование контекстов, provide/inject и прочего, что похоже на изображение ниже:

Главной проблемой этого подхода является отсутствие гибкости, внятного дебага и производительности на хоть сколько-нибудь значимых объёмах данных. Собственно, это привело к созданию инструментов для управления состояниями (например, Redux). Это буквально открыло «ящик Пандоры»:

Управление состоянием и тулинг вокруг него

Стейт-менеджеры — это, по сути, наблюдаемый (observable) объект или объекты, которые живут высокоуровнево, умеют общаться с компонентами, как бы ‭«живя сбоку» от них. Такие инструменты можно условно разделить на 2 категории:

Flux/Redux ориентированные

Концепция следующая. Один источник истины, объект, где лежат данные, умеет пушить в компоненты свои обновления. Например, обновился user name, и объект оповещает все компоненты об изменениях. Чтобы доставить до него изменения, нужно вызвать экшн-функцию, которая даст указание обновиться вашему store.

Сервисы состояний

Сюда входят Mobx и другие observable подобные библиотеки, состоящие из маленьких объектов. Под капотом может быть прокси-объект, способный реагировать на разные изменения или механизм, умеющий опрашивать и отдавать компонентам данные о них. Отличие в том, как хранятся и распространяются данные. Вместо централизованно распределяющего данные объекта мы имеем n-ное количество сервисов, которые поставляют сгруппированные по какому-то принципу данные для вашего слоя представления.

Концепция управления состоянием

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

  • Это всегда один источник истины (Single Source of Truth). При внесении в приложение библиотеки состояний ваши данные должны проходить через неё. Если использовать дополнительные адаптеры, скорее всего, получится рассинхрон — будет очень неудобно тестировать. Важно: централизованно нужно хранить только состояния из бизнес-данных. Флаги переключения состояния компонентов и т.д. лучше держать на уровне контекстов или локально.

  • Односторонний поток данных, обновление через действия (Actions). Мы уже определились с тем, что экшен должен поставлять изменения, а ваши компоненты на них отреагируют. Если вы правильно изолируете слой запроса ручек, правильно сложите это в состояние, то сможете легко мигрировать между React, Vue и прочим.

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

Представление состояний в интерфейсе

Когда мы определились с тем, как получить данные, куда их положить и как агрегировать, перед нами встаёт вопрос о том, как их показать конечному пользователю. Тут мы начнём знакомиться с библиотеками реактивного управления отображением. Я рекомендую почитать статью о том, как работает браузер под капотом (она старая, но полезная) перед тем, как продолжить чтение этого материала.

Сразу оговорюсь, что «реактивность» современного фронтенда никакого отношения к реактивному программированию не имеет — просто слова совпали. Вся магия вычисления происходит под капотом библиотек типа React.

Давайте пройдёмся по характеристикам современного реактивного (реагирующего на изменения) фронтенд-приложения:

  • Обновление UI в ответ на изменения данных. Компоненты реагируют ��а специальные события изменений из внешних источников (локальное состояние — тоже технически внешнее изменение, так как под капотом, например useState, инкапсулирована логика оповещения об изменениях), таких как redux store.

  • Декларативное описание компонентов. Сейчас, по сути, любой компонент является функцией, в контексте исполнения которой присутствует жизненный цикл, зафиксированный контракт возвращаемого в ответ на изменения значения. Грубо говоря, она «видит» произошедшее изменение. Например, задиспатчился эвент, который обновляет определённое состояние. В ответ перевызывается функция и с новыми данными рендерит новый элемент.

  • Односторонний поток данных (One/Two-way Binding). Данные всегда идут вниз по дереву компонентов и вы не можете изменить стейт в обратную сторону, даже если общаетесь между ними. Существует процесс оповещения об изменениях (two-way binding), но это скорее абстракция над императивным присваиванием через коллбэк.

  • Система отслеживания изменений. Это механизм, которого мы уже касались немного выше. Он позволяет реагировать на «реактивный» поток данных изменением отображения. Ниже рассмотрим подробнее.

Система отслеживания изменений

Как уже было сказано выше, это механизм для изменения интрефейса (частичного) в ответ на изменения. Основная его  задача —  крайне эффективный рендер, который подразумевает перерисовку не всего DOM-дерева, а только реально изменённых узлов. Для этого есть несколько устоявшихся подходов:

  • VirtualDOM. В браузер вместе с приложением поставляется код, который создаёт сложное иерархическое представление вашего DOM-дерева в памяти. Первичные изменения оповещают эту структуру о новом состоянии и далее в определённый момент времени (tick) происходит рендер накопленных изменений. Но так как это тяжёлый процесс, нужно поставлять зависимый код, который реально в продуктовой разработке не нужен и в целом не самый эффективный. Со временем были придуманы новые подходы.

  • Детекторы изменений и Zones. В приложении используются специальные зоны (Zones) — области, отслеживающие контекст выполнения кода и возможные побочные эффекты (сайд-эффекты), такие как асинхронные события, таймеры, HTTP-запросы и т.д. Когда происходит такой эффект, детектор изменений автоматически «ловит» его с помощью специального обработчика. Этот обработчик сканирует дерево компонентов, определяет, какие части данных изменились, и формирует карту зависимостей по зонам. После этого происходит перерисовка только тех компонентов, чьи данные реально изменились. Например, в Angular для этого используется Zone.js.

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

Современный фронтенд со всеми системами отслеживания изменений, стейт-менеджерами и т.д. характеризуется следующими свойствами:

  • Иммутабельность данных — это принцип, согласно которому объекты и структуры данных не изменяются напрямую, а при каждом обновлении создаётся их новая версия. Такой подход делает изменения в состоянии более предсказуемыми и позволяет эффективно отслеживать, что именно изменилось. Это особенно важно для производительных перерисовок интерфейса, как, например, в React или Redux. Вместо того чтобы «перетирать» старые значения, приложение всегда работает с копиями, что упрощает сравнение, откат изменений и оптимизацию рендера.

  • Минимизация изменений в реальном DOM. Как можно меньше ходим в DOM-дерево и что-то обновляем, так процесс пересчёта лейаутов, обновления узлов и прочее — крайне дорогой процесс, в котором очень мало оптимизаций. Нужно поставлять изменения как можно позже, сделав все калькуляции где-то в памяти.

  • Дебаунсинг и микро-оптимизации. Вытекает из предыдущего. Это механизм, который позволяет делать накопительные изменения, собирать пачки для обновления и затем «коммитить» их в браузер.

Данные и их передача

Существуют понятия — тонкий и толстый клиент. Из школьного курса информатики следует, что тонкий клиент — это когда обработка произошла на сервере, толстый — если в обратную сторону, например, SPA.

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

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

  • Толстый клиент, в свою очередь, «перетягивает» в браузер нормализацию данных, подготовку, например, на уровне маппинга. Более того, в клиентском коде появляются разные сущности, напрямую не связанные с компонентами, как, например, создание доменных моделей.

И вот в этом месте фронт и бэк часто не понимают друг друга. Основная проблема, как мне кажется, в том, что проектирование происходит не «инженерно», а по разные стороны баррикад.

Архитектурные изменения: что с этим делать и как мы работаем с данными

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

Давайте рассмотрим, что не так с «классическими» подходами:

  • Диктатура разделения ответственности по слоям. Там, где на бэкенде коэффициент внесения изменений в модельные представления низкий (то, что вы можете отложить в use case и прочее), во фронте приведёт к апокалипсису. Изменения постоянны, из-за этого возникают частые протекания и нарушения договорённостей. Сами подходы в обучении фронтов редко встречаются.

  • Локальные состояния компонентов будут всегда. Бизнес-логика однозначно протечёт в компоненты — архитектура уже будет нарушена. Да и в целом в хранении чего-либо в компонентах нет чего-то ужасного. В конечном итоге архитектурный паттерн должен делать разработку достаточно безопасной при условии удобства внесения изменений.

  • Хаотичные появления хранилищ состояний и обработчиков событий. Тут проблема в том, что состояния далеко не всегда используются явно (т.е. через прямые импорты), а хранятся в контекстах, каких-то обёртках и т.д., что неизбежно приводит к протеканиям между слоями.

  • Замедление реакции приложения. Чем больше мы вводим абстракций и усложнений, тем больше появляется потенциальных мест для сбоев, утечек памяти и прочего в таком стиле.

Нельзя сказать, что «классические» архитектурные паттерны плохие. Они просто другие, создавались задолго до появления современного фронтенда и, как следствие, не учитывают его потребности. В ответ на этот вызов начали появляться современные, фронтендо-центричные паттерны:

  • Feature-Sliced Design (FSD). Возможно, вы когда-то слышали о ней. FSD ориентирована на запросы фронтенда. Она вводит понятие среза или slice — это когда структура и архитектура (очень важно разделить, потому что, как мне кажется, FSD — больше структурный паттерн) формируется не доменно-центрично на основе приходящей модели данных, а относительно функционала.


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

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

  • Modular-Isolated Design (MID). Это методология, которую я разрабатываю в свободное время. По сути, основная цель — это организация связей вокруг домена, а не функций. Удобная упаковка слоёв на основе сущностей, при этом строго аннотируя зависимости и внешние управляющие конструкции.

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

Микрофронтенды

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

Сначала фронтендеры «радостно» сбежали из репозиториев бэкенда и смотрели в светлое будущее. По сути, подход основан на поставке в браузер вместе с бизнес-кодом кучи всего для обработки роутинга на лету, состояний и т.д. прямо на клиенте и хождения на бэк только за перезапросом данных. Но довольно быстро (за пару лет) стало понятно, что есть существенные минусы:

  • При больших объёмах SPA превращается в монолит. Всё так — мы сбежали из монолитов, чтобы… изобрести свой монолит. В контексте приложения — куча данных, разделения нет, управлять крайне сложно, ещё и билд становится титанических размеров.

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

  • Нет гибкости для команд. Тут снова про монолиты. По сути, всё в одной репе, сложно версионироваться и откатывать изменения.

  • Сложности с индексацией. Если вам нужно SEO, то начинаются пляски с бубном, возврат к корням — он же SSR, — и прочие приседания.

В ответ на это фронтендеры пошли по пути бэкенда и решили сделать микросервисы. В нашем случае это те же SPA, которые встали в одну страницу: если раньше в одном приложении были orders и users, теперь это два разных приложения, которые объединяются роутингом. Так называемые MPA – Multi Page Application.

И всё же часть проблем осталась:

  • Монолиты становятся меньше, хотя есть свои «но». Да, мы поделились на команды, но не компоненты в системе. Users отделены, но, по сути, это теперь здоровый МКС, который надо бы поделить ещё раз.

  • Чужой домен в репозитории. Так как компоненты пока не выделены, в репу часто приходят гости и контролировать это тяжеловато.

В какой-то момент случилась революция в виде микрофронтендов.

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

  • Каждая реализация — почти гарантированный костыль. Внятного индустриального стандарта практически нет. Да, есть Module Federation. Если вам говорят, что это хороший инструмент — не верьте, костылей будет достаточно уже из коробки. В целом в компаниях делают своих подходы, так как сразу не заводится. С BDUI (SDUI) — схожая история.

  • Основное условие — минимальная коммуникация. Микрофронтенды должны свести общение друг с другом к минимуму через общую шину событий. Такую коммуникацию крайне сложно контролировать, построить контрактное тестирование или типизацию рантайма. По сути, никакого contract first.

  • Дополнительная нагрузка на трафик пользователей. Представьте, что загрузился shell и прилетает набор манифестов. У каждого из них есть огромное количество собственных отдельных ссылок с чанками. Нужно сделать так, чтобы несколько версий React не подгружались по 100−500 раз. Проблема пока не решена до конца и каждый делает свой костыль.

  • Сложно применять аналитику типа Lighthouse, как и в целом метрики. По сути, микрофронтенды лишают вас возможности предсказуемо собирать метрики производительности и индексации.

  • Раздача манифестов и инициализация требуют отдельных внятных SLO. «Робобаба» будет звонить стабильно: у этой штуки есть инициализация, запрос, может развалиться сеть, её надо интегрировать, она умеет отваливаться, нет коммуникации — и со всем этим жить вашей команде.

FrontOps

Всё, что описано выше, ещё нужно как-то собрать, и эти задачи часто лежат за пределами рядовых обязанностей продуктового разработчика. Это выделенная роль в операционных аспектах, которые сложно контролировать даже фронтендерам. Если упростить, то в какой-то момент DevOps дорвались до фронтенда, посмотрели на это и сказали: «Ну вас, вот вам Docker-контейнер — живите с этим сами».

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

Приведу примеры зон ответственности:

  • Инструменты сборки. Приложений может быть много, но их всех нужно собрать — например, с помощью Webpack.

  • Статанализ (линтеры, приттеры, sonar). Это всё инструменты разработки, которые проверяют наш код на соответствие стандартам и т.д.

  • Настройка тулинга для версионирования, git.

  • CDN, работа с ресурсами, прокси и т.д..

  • Интеграция и настройка мониторинга (Web Vitals, Performance Core). 

  • Деплой, контейнеры, оркестрация.

  • Построение CI/CD и процессы вокруг.

Заключение

Подытоживая всё вышесказанное, очень хочется напомнить, что инженерные практики на бэкенде формировались десятилетиями, а фронтенд только последние 10 лет в более-менее серьёзном состоянии. 

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

Надеюсь, эта статья послужит глоссарием к изучению. А кроме того, вы можете расширить свои знания, посетив TeamLead Conf 2025 — это единственная профессиональная конференция для тимлидов и руководителей не только из IT.

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


  1. ooko
    03.10.2025 09:39

    Каринка про гуся это реалии современного реакта. И на мой взгляд было бы все проще если не пара вещей: иммутабельность - зло с которого все началось, shouldComponentUpdate|memo - который блокирует всю ветку а не сам узел.


  1. noavarice
    03.10.2025 09:39

    Сам бэк, немного занимаюсь фронтом в своем проекте, поэтому интересует фронтовая архитектура. Для меня это очень полезная обзорная статья, спасибо


    1. powerman
      03.10.2025 09:39

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