Во вторник в 10:14 утра ни одна метрика не была красной.

  • Задержка API p95: +3 мс (шум).
  • Частота ошибок: 0,21%0,23%.
  • CPU: без изменений.
  • Память: без изменений.
  • CI: зеленый.

Но поведение пользователей говорило об обратном:

  • Продолжительность сеансов сократилась на 17%.
  • Количество нажатий «Назад» выросло на 28%.
  • Обращений в службу поддержки стало вдвое больше — все расплывчатые: «Тормозит», «Страницы прыгают», «Интерфейс выводит меня из себя».

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

К концу квартала это Angular-приложение работало без двух фронтенд-инженеров — не потому, что мы упростили систему, а потому что она перестала создавать для нас проблемы.

Вот как это произошло.

«Всё работает» было технически правдой

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

  • Lighthouse-баллы: приемлемые;
  • размер бандла: тенденция к снижению;
  • Change Detection (механизм отслеживания изменений): «оптимизирован»;
  • Состояние: «хорошо структурировано».

Код выглядел дисциплинированным. Ревью проходили. Ничего очевидно плохого не было.

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

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

  • «Починить дерганье при навигации назад».
  • «Почему этот компонент перерисовывается?».
  • «Почему фильтр опять сброшен?».
  • «Почему эта страница со временем становится все тяжелее?»

Ни одна из них не была аварийной. Это были утечки внимания.

Первый график, который заставил нас нервничать

Наконец мы построили график того, что игнорировали годами.

Перерисовка компонентов при каждой навигации, выборка в производственной среде:

Route            Median Re-renders       p95        Notes
===================================================================
/dashboard        142                    311        No data change
/orders           98                     227        Cached response
/settings         64                     141        Static form

По отдельности ничего страшного.

Но ничто не должно перерисовываться 311 раз только потому, что пользователь нажал «Назад».

Это была не ошибка производительности. Это была проблема моделирования.

Мы перепутали структуру со стабильностью

Приложение «чисто» развивалось в течение четырех лет:

  • Повсеместные общие сервисы.
  • Умные контейнеры, глупые компоненты.
  • RxJS-потоки «для согласованности”.
  • ChangeDetectionStrategy.OnPush (в основном).

По отдельности — все разумно.

В совокупности приложение вело себя как распределенная система без границ.

Состояние было:

  • технически централизованным;
  • практически вездесущим;
  • эмоционально изнуряющим для понимания.

Небольшое изменение в UI требовало:

  • затронуть три сервиса;
  • настроить две подписки;
  • перетестировать пять экранов «на всякий случай».

Работа была не сложной. Она была нескончаемой.

Момент, когда мы поняли, что приложение нанимает инженеров

Мы собрали PR-запросы, относящиеся к фронтенду, за шесть месяцев и разбили их по категориям:

Category                              % of PRs
===========================================================
New product capability                41%
Bug fixes                             37%
"UI consistency / cleanup"            22%

Последние 22% были убийственны.

Не функции. Не ошибки, которые могут описать пользователи. Просто управление энтропией.

Приложение масштабировалось не с пользователями — оно масштабировалось с дополнительными когнитивными усилиями.

Артефакт, изменивший наше мышление

Вот реальный фрагмент, упрощенный, но структурно точный:

this.vm$ = combineLatest([
  this.route.params,
  this.filters$,
  this.user$,
  this.permissions$
]).pipe(
  map(([params, filters, user, perms]) => {
    if (!perms.canView(params.id)) {
      return { state: 'forbidden' };
    }

return {
      state: 'ready',
      data: computeView(params, filters, user)
    };
  })
);

Ничего «неправильного».

Пока не понимаешь:

  • filters$ обновляется при наведении курсора;
  • user$ обновляется незаметно;
  • permissions$ перезапускается при каждом обновлении аутентификации;
  • route.params выводится при обратной навигации.

В результате представление пересчитывалось — полностью — по причинам, не связанным с экраном.

Мы реагировали не на намерения пользователя. Мы реагировали на фоновый шум системы.

Простое решение: меньше потоков, больше ответственности

Мы не переписывали приложение. Не меняли фреймворк. Не внедряли новые библиотеки для состояния.

Мы сделали три малозаметные вещи:

  1. Состояние по умолчанию перестало пересекать границы функций.
  2. Большинство потоков стали локальными, а не общими.
  3. Навигация снова стала границей жизненного цикла.

Вот и все.

Меньше «хитростей». Больше явной ответственности.

Результатом стало не только меньшее количество перерисовок, но и меньшее количество вопросов.

Когда дэшборд наконец отразил изменения

Три недели спустя:

  • Среднее количество перерисовок на навигацию: 14237.
  • Рост памяти при длительных сеансах: без изменений.
  • Обращения в поддержку: -31%.
  • PR-запросы по исправлению ошибок на фронтенде: заметно меньше.

Но настоящий сигнал пришел позже.

Два фронтенд-инженера покинули команду: один перешел в инфраструктуру, другой — в новый продукт.

Мы никого не наняли на их должности.

Работа по-прежнему делалась. Спокойнее. С меньшим количеством «мелких правок», преследующих нас в каждом спринте.

От чего мы отказались (потому что не обошлось без компромиссов)

Это не далось бесплатно.

  • Меньше глобального переиспользования.
  • Больше «скучного» дублирования.
  • Меньше абстрактных потоков.
  • Местами код стал менее элегантным.

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

Но эти изменения намеренные, а не случайные.

Взамен мы получили:

Вместо «Менять что угодно откуда угодно» — «Менять меньше вещей с уверенностью».

Для системы, уже находящейся в продакшен-среде, это был разумный компромисс.

Извлеченные уроки

Споры о фреймворках в данной ситуации не имели значения. Паттерны нас не спасли. Лучшие практики нас тоже не подвели.

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

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

Если UI продолжает поставлять вам работу, это:

  • не проблема найма;
  • не проблема кадров;
  • это, скорей всего, проблема границ моделирования.

Намеренно спокойное заключение

Приложение до сих пор не идеально. Мы по-прежнему допускаем ошибки. Мы по-прежнему спорим при ревью кода.

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

И это та победа, которую замечаешь, только когда ее добиваешься.


Читайте также:

Читайте нас в Telegram, VK и Дзен


Перевод статьи Computer Architect: The Angular App That Needed 2 Fewer Frontend Engineers

Предыдущая статьяСтоит ли заниматься iOS-разработкой в 2026 году?