К содержимому
--:--
местное время
EVS.CODER / navigation

Навигация

На связи

TelegramGitHubLinkedIn

ENGINEERING NOTES / Архитектура

Масштабирование frontend за пределами компонентов

Почему компонентного подхода недостаточно и как сохранить скорость разработки при росте продукта.

Статья на русском языке

Компоненты — начало, а не архитектура

React, Angular и Vue научили нас мыслить интерфейсами как деревом компонентов. Но дерево отображения не описывает границы бизнес-логики, владение данными, контракты API и ответственность команд. На небольшом проекте эти решения можно принимать по ситуации; на большом каждое неявное соглашение превращается в дополнительную стоимость изменения.

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

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

Признаки того, что архитектура не успевает за продуктом

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

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

**Проверьте:**

  • Можно ли объяснить владельца каждого состояния?
  • Может ли модуль изменить внутреннее устройство без правок у потребителей?
  • Проверяются ли ограничения импортов автоматически?
  • Есть ли повторяющиеся исключения, которые команда копирует вручную?

Границы по предметной области

Структура `components/`, `hooks/`, `services/`, `utils/` группирует код по технической роли, но разносит одну пользовательскую возможность по всему репозиторию. При росте продукта полезнее группировать связанные части вокруг каталога, поиска, корзины, авторизации и оформления заказа. Внутри такой области могут жить UI, модель, API и тесты.

Это один из вариантов, а не обязательная схема. Feature-Sliced Design формализует слои, срезы и направление зависимостей, но даже его документация рекомендует начинать с простого разделения и вводить новые уровни по мере необходимости. Модуль должен возникать из реальной границы изменений, а не ради заполнения архитектурного шаблона.

TypeScript / пример
src/
  app/                 # запуск, маршруты, провайдеры
  pages/               # композиция страниц
  features/            # пользовательские действия
    add-to-cart/
      ui/
      model/
      api/
      index.ts
  entities/            # предметные сущности
    product/
      ui/
      model/
      api/
      index.ts
  shared/              # инфраструктура и универсальный UI

Публичный API модуля

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

Сам `index.ts` не обеспечивает защиту: обходные импорты нужно ограничивать ESLint, правилами пакетов или инструментами анализа зависимостей. Особенно опасны циклы: если корзина зависит от каталога, каталог от профиля, а профиль от корзины, отдельные области уже нельзя безопасно развивать независимо. Общие контракты иногда следует вынести ниже, а координацию нескольких возможностей — поднять в композиционный слой.

**Практика:** фиксируйте публичные экспорты, проверяйте циклы в CI и обсуждайте изменение контрактов как изменение API, а не как локальный рефакторинг.

TypeScript / пример
// entities/product/index.ts
export type { Product } from './model/types';
export { ProductCard } from './ui/ProductCard';
export { getProduct } from './api/getProduct';

// Потребитель
import { ProductCard, type Product } from '@/entities/product';

Состояние: найдите владельца данных

Состояние бывает разным. Открытый dropdown — локальное UI-состояние. Результаты API — серверное состояние с кэшированием и актуальностью. Фильтры каталога часто должны жить в URL, чтобы поддерживать ссылки и историю браузера. Данные многошаговой формы могут принадлежать одному пользовательскому сценарию. Смешивание этих категорий в одном глобальном store создаёт ненужные связи.

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

TypeScript / пример
type CatalogFilters = {
    query: string;
    sort: 'popular' | 'price_asc' | 'price_desc';
    page: number;
};

function readFilters(params: URLSearchParams): CatalogFilters {
    const requestedSort = params.get('sort');
    const sort = requestedSort === 'price_asc' || requestedSort === 'price_desc'
        ? requestedSort
        : 'popular';

    return {
        query: params.get('query') ?? '',
        sort,
        page: Math.max(1, Number(params.get('page')) || 1),
    };
}

API и защита предметной модели

Ответ backend — внешний контракт, а не обязательно идеальная модель интерфейса. Если компонентам напрямую передавать DTO с нестабильными полями, изменение API затронет множество экранов. Адаптер позволяет нормализовать ответ на границе.

TypeScript проверяет типы во время разработки, но не валидирует произвольный JSON в runtime. Для важных границ применяйте схемы проверки или генерацию типов из OpenAPI с необходимой runtime-валидацией. Не создавайте адаптер для каждого тривиального ответа: дополнительный слой оправдан, если он изолирует реальную нестабильность или переводит внешнюю модель в понятную внутреннюю.

TypeScript / пример
type ProductDTO = {
    product_id: number;
    product_name: string;
    price_amount: string;
};

type Product = { id: string; title: string; price: number };

function mapProduct(dto: ProductDTO): Product {
    const price = Number(dto.price_amount);
    if (!Number.isFinite(price)) throw new Error('Invalid price');
    return { id: String(dto.product_id), title: dto.product_name, price };
}

Производительность — архитектурное свойство

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

Ищите сетевые водопады, параллельно запрашивайте независимые данные, оптимизируйте hero-изображения и шрифты. Проверяйте Core Web Vitals и реальные пользовательские данные, а не только локальный Lighthouse. Оптимизация имеет смысл, когда измерение показывает улучшение важного сценария.

TypeScript / пример
import { lazy, Suspense } from 'react';

const AnalyticsPage = lazy(() => import('./pages/AnalyticsPage'));

export function AnalyticsRoute() {
    return (
        <Suspense fallback={<div>Загрузка аналитики…</div>}>
            <AnalyticsPage />
        </Suspense>
    );
}

Архитектура для нескольких команд

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

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

Как развивать архитектуру без большого переписывания

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

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

Ещё одна заметкаDesign System, которая выдерживает давление продукта