Интенсив по Frontend System Design для Senior-разработчиков

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

1. Анатомия Frontend System Design интервью: цели, ожидания и критерии оценки

Анатомия Frontend System Design интервью: цели, ожидания и критерии оценки

Парадокс: более 60% опытных Senior-разработчиков проваливают свое первое интервью по Frontend System Design, несмотря на то, что в повседневной работе они успешно создают сложные React-приложения. Причина кроется не в нехватке технических знаний, а в фундаментальном непонимании правил игры. Когда кандидата просят спроектировать ленту новостей, он часто начинает рисовать структуру компонентов или описывать стейт-менеджер. Но интервьюер ждал совершенно другого.

Чтобы успешно пройти эту секцию, необходимо сперва понять, зачем она вообще существует и как именно вас оценивают.

Иллюзия знакомого формата

Для большинства разработчиков привычным форматом проверки знаний является алгоритмическая секция (Live Coding) или вопросы по специфике фреймворка. System Design кардинально отличается от них по своей природе.

Алгоритмическая задача имеет четкое условие и объективно правильный ответ (или оптимальную асимптотику). System Design задача намеренно лишена четких границ. Это задача открытого типа, где правильного ответа не существует в принципе.

Сравним два формата, чтобы зафиксировать этот сдвиг парадигмы:

| Критерий | Алгоритмическое / Coding интервью | Frontend System Design интервью | | :--- | :--- | :--- | | Вводные данные | Четкие, исчерпывающие условия. | Размытые требования (намеренно). | | Фокус кандидата | Написание рабочего, оптимального кода. | Сбор требований, архитектура, компромиссы. | | Роль интервьюера | Экзаменатор, проверяющий решение. | Коллега, с которым вы обсуждаете проект. | | Результат | Работающий алгоритм / компонент. | Высокоуровневая схема системы и API. |

Главная ошибка на FSDI — попытка «написать код» в уме. System Design — это не про то, как реализовать кнопку или хук. Это про то, какие технологии выбрать, как компоненты системы общаются по сети, где хранятся данные и почему выбран именно такой подход для конкретных бизнес-требований.

Главная цель интервьюера

Интервьюер не пытается подловить вас на незнании специфического API браузера. Его глобальная цель — ответить на один вопрос: «Смогу ли я доверить этому человеку проектирование критически важной системы в условиях неопределенности, и будет ли мне комфортно с ним работать?»

Интервью — это симуляция первого дня работы над новым крупным эпиком. К вам приходит продакт-менеджер и говорит: «Нам нужен клон Pinterest». Ваша задача — превратить эту абстрактную фразу в конкретный инженерный план. Интервьюер оценивает вашу способность справляться с неопределенностью (ambiguity), структурировать хаос и вести техническую дискуссию.

Анатомия оценки: как собираются «сигналы»

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

Оценка на FSDI обычно строится вокруг трех ключевых осей.

!Матрица оценки кандидата на Frontend System Design интервью

1. Продуктовое мышление и сбор требований (Product & Requirements)

Senior-разработчик не бросается проектировать систему, пока не поймет, для кого и зачем она создается.
  • Позитивный сигнал: Кандидат задает уточняющие вопросы. Какая ожидается нагрузка (DAU)? Каково соотношение чтения и записи? Нужно ли поддерживать оффлайн? Целевая аудитория сидит с быстрых Mac или с бюджетных смартфонов на 3G?
  • Негативный сигнал: Кандидат делает допущения молча или сразу переходит к рисованию квадратиков баз данных и серверов.
  • 2. Техническая глубина и архитектура (Architecture & Technical Depth)

    Здесь оценивается ваше понимание паттернов, сетей, рендеринга, управления состоянием и производительности.
  • Позитивный сигнал: Кандидат разделяет систему на логические блоки, определяет контракты API (REST, GraphQL, WebSocket), продумывает стратегию кэширования и нормализации данных.
  • Негативный сигнал: Использование модных технологий без обоснования («Давайте возьмем Redux Toolkit и GraphQL, потому что это стандарт»).
  • 3. Коммуникация и лидерство (Communication & Leadership)

    System Design — это диалог. Вы должны вести интервьюера за собой, а не ждать от него подсказок.
  • Позитивный сигнал: Кандидат озвучивает свои мысли вслух (think out loud), проактивно предлагает варианты, адекватно реагирует на критику и новые вводные от интервьюера.
  • Негативный сигнал: Кандидат замолкает на 5 минут, рисует схему, а потом выдает ее как финальный результат, не объясняя логику принятия решений.
  • Ожидания от Senior-уровня: искусство компромиссов

    Ключевое отличие Middle-разработчика от Senior на секции System Design заключается в отношении к технологиям. Middle знает, как применить технологию. Senior знает, когда ее применять, а когда — нет.

    В инженерии программного обеспечения существует золотое правило:

    > Идеальных архитектурных решений не существует. Существуют только компромиссы (trade-offs) в контексте конкретных бизнес-требований.

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

    Пример из практики: Допустим, стоит задача спроектировать сетевой слой для дашборда с графиками.

  • Ответ уровня Middle: «Я буду использовать GraphQL, потому что он решает проблему overfetching (избыточной загрузки данных) и позволяет фронтенду запрашивать только то, что нужно. Это современно и удобно».
  • Ответ уровня Senior: «Мы можем использовать GraphQL, чтобы минимизировать payload и дать гибкость клиенту. Однако, поскольку это дашборд, нам важна скорость первичной загрузки. GraphQL делает HTTP-кэширование на уровне CDN крайне сложным, так как все запросы идут через POST на один эндпоинт. Если графики обновляются раз в сутки, классический REST с GET-запросами позволит нам агрессивно кэшировать ответы на CDN, что критически снизит нагрузку на бэкенд и ускорит TTFB (Time to First Byte). Учитывая требования, я выбираю REST».
  • Во втором случае кандидат показал понимание сети, кэширования и связал техническое решение с бизнес-метриками. Именно такого уровня аргументации от вас ждут.

    Смена парадигмы

    Подготовка к Frontend System Design требует смены рабочей парадигмы. Вы должны перестать мыслить категориями useEffect, div и CSS-in-JS. Ваш новый инструментарий — это протоколы передачи данных, стратегии рендеринга, паттерны кэширования и метрики производительности.

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

    10. Синтез знаний: практический разбор типового архитектурного кейса на интервью

    Синтез знаний: практический разбор типового архитектурного кейса на интервью

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

    В этой финальной главе мы не будем изучать новые концепции. Мы возьмем знания из предыдущих девяти модулей — от метрик и рендеринга до микрофронтендов и безопасности — и проведем их через горнило реального System Design интервью. Наша цель — увидеть сквозную линию мысли Senior-разработчика.

    Шаг 1: Разрешение неопределенности (Сбор требований)

    Задание «спроектируйте Netflix» намеренно абстрактно. Если вы сразу начнете рисовать квадратики компонентов, вы провалите секцию. Ваша первая задача — сузить скоуп.

    Вы задаете вопросы интервьюеру, чтобы зафиксировать рамки системы:

  • Вы: «Платформа предназначена для VOD (Video on Demand) или включает Live-трансляции?»
  • Интервьюер: «В основном VOD, но у нас есть премьеры, где пользователи смотрят видео одновременно и общаются в Live-чате».
  • Вы: «Каков масштаб? Насколько важен SEO?»
  • Интервьюер: «10 миллионов DAU (Daily Active Users). Каталог публичный, SEO критически важен для привлечения трафика из поисковиков».
  • Выписываем на доску согласованные требования:

    | Функциональные требования | Нефункциональные требования | | :--- | :--- | | 1. Главная страница с персонализированным каталогом. | 1. Глобальная доступность, быстрая загрузка (SEO). | | 2. Страница просмотра видео (плеер). | 2. Устойчивость к пиковым нагрузкам во время премьер. | | 3. Live-чат реального времени для премьер. | 3. Изоляция сбоев (падение чата не должно ломать плеер). |

    Шаг 2: Высокоуровневая архитектура и стратегия доставки

    Имея вводные, мы принимаем первые глобальные компромиссы.

    Организация команд и кодовой базы Учитывая масштаб (10 млн DAU) и разнородность функционала, над проектом явно будут работать десятки команд. Монолит здесь превратится в узкое горлышко. Вы предлагаете архитектуру микрофронтендов. Система делится на независимые домены: Catalog MFE, Player MFE и Social MFE (чат). Они разрабатываются в монорепозитории для переиспользования UI-кита, но деплоятся независимо.

    Стратегия рендеринга Вы вспоминаете конфликт метрик доставки. Нам нужен SEO для каталога, но мы не можем рендерить каждую страницу на сервере (SSR) из-за колоссальной нагрузки от 10 млн пользователей.

    Вы озвучиваете гибридный подход: > «Для страниц каталога мы используем ISR. Страницы генерируются статически и кэшируются на узлах CDN (Edge Computing). Это обеспечит минимальный TTFB и отличный LCP для поисковых ботов. > Плеер и Live-чат — это сугубо интерактивные зоны за пейволлом или требующие авторизации. Их мы будем рендерить через CSR, подгружая лениво, чтобы не блокировать главный поток при старте приложения».

    Шаг 3: Проектирование сетевого слоя и API

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

  • Каталог (Catalog MFE): Здесь идеально подходит GraphQL. На главной странице нам нужны только постеры и названия, а на странице деталей — актеры, описание и рейтинг. GraphQL предотвратит Overfetching при загрузке списков и Underfetching при открытии деталей.
  • Плеер (Player MFE): Для получения манифеста видео и отправки аналитики просмотра достаточно классического REST, так как контракты здесь плоские и стабильные.
  • Live-чат (Social MFE): Здесь критична двунаправленная связь с низкой задержкой. Вы выбираете WebSockets.
  • Управление состоянием: Вы объясняете интервьюеру, что глобальный стейт-менеджер (вроде Redux) на уровне всего приложения нам не нужен — это нарушит изоляцию микрофронтендов. Вместо этого каждый MFE управляет своим серверным кэшем. Если пользователю нужно обновить аватарку, и это должно отразиться в Social MFE и шапке Catalog MFE, вы используете Event Bus для передачи легковесного события, а не шарите объект пользователя в памяти.

    Шаг 4: Глубокое погружение (Компонент Live-чата)

    Интервьюер просит углубиться в самую технически сложную часть: Live-чат во время популярной премьеры. Вводная от интервьюера: «На премьере 500 000 человек. В чат летит 2000 сообщений в секунду. Как фронтенд с этим справится?»

    Здесь вы должны продемонстрировать понимание работы браузера и защиты метрики INP. Если рендерить каждое сообщение, главный поток (Main Thread) будет заблокирован Long Tasks, и вкладка зависнет.

    Вы предлагаете многоуровневую оптимизацию:

  • Виртуализация (Windowing): В DOM всегда находится ровно столько узлов, сколько помещается во вьюпорт (например, 20 сообщений).
  • Throttling UI updates (Батчинг на клиенте): Мы не пускаем сообщения в стейт по одному. Мы накапливаем их в буфере и сбрасываем в UI пачками через requestAnimationFrame.
  • Вы можете выразить логику накопления математически: при мс, где — количество сообщений, добавляемых за один кадр, — текущий размер буфера, а — лимит, превышение которого вызовет пропуск кадров.
  • Web Workers: Парсинг входящих JSON по WebSocket и фильтрацию мата мы выносим в фоновый поток (Web Worker), чтобы освободить главный поток исключительно под отрисовку.
  • Шаг 5: Надежность, безопасность и мониторинг

    В последние 10 минут интервью вы показываете, что мыслите категориями продакшена, а не просто локальной разработки.

    Изящная деградация (Graceful Degradation): Что если WebSocket-сервер чата упадет под нагрузкой? Вы оборачиваете Social MFE в Error Boundary. > «Если чат падает, Error Boundary перехватывает ошибку и показывает заглушку "Чат временно недоступен". Главное — это не должно затронуть Player MFE. Пользователь должен продолжить смотреть видео. Это принцип изящной деградации».

    Безопасность: Чат — главный вектор для XSS-атак. Помимо экранирования строк в самом фреймворке, вы декларируете использование строгой Content Security Policy (CSP), запрещающей unsafe-inline скрипты, чтобы даже в случае уязвимости в коде парсера злоумышленник не смог выполнить скрипт в браузере жертвы.

    Обсервабилити: Для отслеживания проблем с воспроизведением вы упоминаете использование Correlation ID. При запросе манифеста видео фронтенд генерирует уникальный ID и передает его в заголовках. Если плеер зависнет, этот же ID уйдет в систему логирования, позволяя инженерам отследить судьбу запроса сквозь все балансировщики и микросервисы.

    Заключение курса

    Успешное прохождение секции System Design — это не поиск единственно верного ответа. Как мы увидели в этом разборе, это способность жонглировать компромиссами. Вы выбрали ISR вместо SSR ради производительности, пожертвовав свежестью данных в миллисекундах. Вы выбрали микрофронтенды ради масштабирования команд, сознательно приняв усложнение CI/CD пайплайнов и инфраструктуры кэширования remoteEntry.js.

    Каждая технология, от Service Worker до GraphQL, от Anycast маршрутизации до стратегий гидратации — это лишь инструмент. Senior-разработчик отличается от Middle тем, что знает не только как работает инструмент, но и когда его применение принесет бизнесу больше пользы, чем вреда.

    Теперь ваш архитектурный арсенал полностью укомплектован. Удачи на интервью!

    2. Универсальный фреймворк решения архитектурных задач: пошаговый алгоритм проектирования

    Универсальный фреймворк решения архитектурных задач: пошаговый алгоритм проектирования

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

    В этой статье мы разберем универсальный пятишаговый алгоритм прохождения Frontend System Design интервью. Он работает как воронка: мы начинаем с самого широкого контекста бизнеса и постепенно сужаем фокус до конкретных технических метрик и узких мест.

    !Воронка Frontend System Design: от бизнес-требований к техническим деталям

    Шаг 1. Сбор требований и определение границ (5–7 минут)

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

    Требования делятся на две категории, и Senior-разработчик обязан прояснить обе.

    Функциональные требования (Что делает система?)

    Это бизнес-фичи. Задавайте вопросы, чтобы ограничить скоуп. Возьмем наш пример с лентой новостей:
  • Поддерживаем ли мы только текст, или нужны фото/видео?
  • Есть ли вложенные комментарии?
  • Лента обновляется в реальном времени (push) или по запросу пользователя (pull)?
  • Нефункциональные требования (Как работает система?)

    Это технические ограничения и метрики качества. Именно здесь закладывается фундамент для будущих компромиссов.
  • Аудитория и нагрузка: Сколько активных пользователей? (Определяет подход к кэшированию).
  • Производительность: Какое целевое время загрузки? Например, Time to Interactive (TTI) сек на 3G-сети.
  • Поддержка устройств: Mobile-first или Desktop-first?
  • Оффлайн-режим: Должна ли лента быть доступна без сети?
  • > Искусство сбора требований — это умение сказать «нет». Если вы попытаетесь спроектировать полномасштабный Facebook за 45 минут, вы не спроектируете ничего. Договоритесь с интервьюером: «Давайте сегодня сфокусируемся на бесконечном скролле текстовых постов с картинками и лайках, а систему комментариев оставим за рамками».

    Шаг 2. Высокоуровневая архитектура (5–10 минут)

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

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

  • Клиентское приложение: (Браузер / PWA).
  • CDN (Content Delivery Network): Для раздачи статики (JS, CSS, изображения).
  • Web Server / BFF (Backend for Frontend): Слой, который агрегирует данные для клиента.
  • API / Микросервисы: Источники данных.
  • На этом шаге вы принимаете первое глобальное решение — стратегию рендеринга. Если нефункциональные требования гласят, что для ленты новостей критически важен SEO и быстрый First Contentful Paint (FCP), вы заявляете Server-Side Rendering (SSR). Если это закрытый B2B-дашборд — выбираете Client-Side Rendering (CSR). (Подробный выбор стратегий мы разберем в Главе 4).

    Шаг 3. Модель данных и сетевое взаимодействие (10 минут)

    Архитектура UI всегда вторична по отношению к данным. Прежде чем рисовать компоненты, нужно понять, как данные структурированы и как они передаются по сети.

    Проектирование API-контрактов

    Определите, какие эндпоинты нужны клиенту. Для ленты новостей это может быть:
  • GET /feed?cursor=12345&limit=20 — получение постов (используем курсорную пагинацию, а не offset, чтобы избежать дублей при добавлении новых постов).
  • POST /posts/{id}/like — постановка лайка.
  • Здесь же обсуждается протокол: REST, GraphQL или WebSocket. Если мы договорились на Шаге 1, что лента обновляется в реальном времени, вы обосновываете использование WebSocket или Server-Sent Events (SSE). (Глубокое погружение в API ждет нас в Главе 6).

    Состояние клиента (State Shape)

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

    Сравните два подхода:

    | Вложенная структура (Антипаттерн для сложных UI) | Нормализованная структура (Senior подход) | | :--- | :--- | | Массив объектов, где автор и комментарии вложены внутрь поста. | Плоские словари (хэш-таблицы), связанные по ID. | | Обновление имени автора требует обхода всего массива постов: сложность . | Обновление имени автора происходит по ключу users[id]: сложность . | | Высокий риск рассинхронизации данных в разных частях экрана. | Единый источник истины (Single Source of Truth). |

    Пример нормализованного стейта:

    Шаг 4. Компонентная архитектура (10 минут)

    Только теперь мы переходим к тому, с чем Frontend-разработчики работают каждый день — к UI. Но на System Design интервью мы не обсуждаем пропсы конкретной кнопки. Мы проектируем дерево компонентов и потоки данных.

    Разбейте интерфейс на крупные логические блоки. Для ленты новостей:

  • App (точка входа, инициализация роутинга и глобального стейта).
  • FeedContainer (Smart-компонент: слушает скролл, запрашивает данные, управляет пагинацией).
  • PostList (Dumb-компонент: рендерит список).
  • PostItem (Отображение конкретного поста).
  • Главный вопрос этого этапа: где живет состояние и как оно передается вниз? Вы должны объяснить, какие компоненты подписаны на глобальный стор, а какие получают данные через props, чтобы избежать лишних ререндеров всей страницы при лайке одного поста.

    Шаг 5. Глубокое погружение и оптимизации (10 минут)

    Последний этап — это стресс-тест вашей системы. Интервьюер выберет одно или два узких места (bottlenecks) и попросит их решить. Это проверка вашей глубины.

    В случае с лентой новостей типичные проблемы:

  • Утечка памяти и падение FPS: Если пользователь проскроллит 1000 постов, DOM-дерево разрастется, и браузер начнет тормозить. Решение: Виртуализация списка (Windowing) — рендерим только те элементы, которые находятся во viewport плюс небольшой буфер (например, DOM-узлов независимо от длины истории).
  • Оптимизация изображений: Ленивая загрузка (Lazy Loading), использование современных форматов (WebP/AVIF), адаптивные размеры через srcset.
  • Оптимистичный UI: При нажатии на лайк мы не ждем ответа от сервера, а мгновенно закрашиваем сердечко, отправляя запрос в фоне. Если сервер вернет ошибку — откатываем состояние и показываем тост.
  • Динамика интервью: управление временем

    Фреймворк — это не жесткие рельсы, а карта. Ваша задача — управлять временем так, чтобы успеть пройти все этапы. Если вы потратите 25 минут на обсуждение цвета кнопок в компонентах, вы не дойдете до оптимизаций производительности, где кроются главные сигналы Senior-уровня.

    !Управление временем на Frontend System Design интервью

    Успешное прохождение секции System Design — это демонстрация того, что вы можете взять хаос бизнес-идеи, структурировать его через сбор требований, выстроить надежный фундамент из данных и сетевых протоколов, обернуть это в масштабируемый UI и защитить от падений под нагрузкой. В следующих главах мы начнем детально разбирать каждый из этих слоев, начиная с глобальной архитектуры и микрофронтендов.

    3. Архитектура крупных приложений: от монолитов и монорепозиториев к микрофронтендам

    Архитектура крупных приложений: от монолитов и монорепозиториев к микрофронтендам

    Представьте: ваша команда выросла с 5 до 50 frontend-разработчиков. Внезапно сборка проекта начинает занимать 40 минут, каждый релиз сопровождается мучительным разрешением конфликтов слияния (merge conflicts), а ошибка стажера в компоненте футера полностью ломает страницу оформления заказа. Производительность падает, хотя людей стало больше. На уровне Senior System Design вы должны понимать: это не техническая проблема React или Webpack. Это проблема масштабирования организационных границ, которая требует архитектурного вмешательства.

    На интервью по проектированию систем для Senior-позиций вас обязательно проверят на умение выбирать глобальную архитектуру приложения. Главный вопрос здесь звучит не «как написать этот код?», а «как разделить этот код так, чтобы независимых команд могли работать, не блокируя друг друга?».

    Закон Конвея как отправная точка

    Любой разговор об архитектуре крупных приложений неразрывно связан с устройством самой компании.

    > Организации, проектирующие системы, ограничены дизайном, который копирует структуру коммуникаций в этой организации. > > Закон Конвея (Melvin Conway, 1967)

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

    Эволюция кодовой базы: от Полирепозитория к Монорепозиторию

    Когда монолит становится слишком большим, первая инстинктивная реакция — разбить его на независимые NPM-пакеты и разложить по разным репозиториям (Polyrepo).

    На первый взгляд, это решает проблему: у каждой команды свой репозиторий, свои пайплайны, свой релизный цикл. Но на практике это порождает Dependency Hell (ад зависимостей). Если вы обновляете дизайн базовой кнопки в ui-kit, вам нужно:

  • Опубликовать новую версию ui-kit.
  • Обойти 15 репозиториев микроприложений.
  • В каждом обновить версию зависимости, прогнать тесты и сделать релиз.
  • Чтобы решить проблему шаринга кода без боли полирепозиториев, индустрия пришла к Монорепозиториям (Monorepo).

    Монорепозиторий — это не просто «весь код в одной папке». Это единый репозиторий, управляемый специализированными инструментами (Nx, Turborepo, Lerna), которые обеспечивают строгие границы между пакетами и умное кэширование.

    | Характеристика | Полирепозиторий (Polyrepo) | Монорепозиторий (Monorepo) | | :--- | :--- | :--- | | Переиспользование кода | Сложно (через публикацию NPM-пакетов) | Легко (прямые импорты локальных пакетов) | | Рефакторинг | Затрагивает множество репозиториев, легко сломать контракты | Атомарные коммиты: изменение API и его потребителей в одном PR | | Инструментарий | Дублируется в каждом репозитории (ESLint, Jest, Webpack) | Единый стандарт на весь проект | | Скорость CI/CD | Быстро (собирается только один мелкий проект) | Требует умного кэширования (собирается только то, что изменилось) |

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

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

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

    Когда независимость релизов становится критическим бизнес-требованием (например, в e-commerce гигантах), монолитного деплоя становится недостаточно. Здесь на сцену выходят Микрофронтенды (Micro-frontends, MFE).

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

    !Сравнение границ развертывания в Монорепозитории и Микрофронтендах

    Ключевое отличие MFE от просто компонентного подхода — это Runtime-интеграция. Микрофронтенды не собираются вместе на этапе сборки (Build-time). Они загружаются и объединяются прямо в браузере пользователя.

    Подходы к реализации Микрофронтендов

    На интервью вас могут спросить, как именно вы бы технически реализовали MFE. Существует три основных паттерна:

  • Iframes (Изоляция на уровне браузера).
  • Самый старый и надежный способ. Каждый микрофронтенд — это отдельный HTML-документ в <iframe>. Плюсы: Идеальная изоляция стилей (CSS) и глобальных переменных; невозможность «уронить» соседнее приложение. Минусы: Ужасный UX (проблемы с модальными окнами поверх фреймов), сложность SEO, тяжелая коммуникация через postMessage, дублирование общих библиотек (React загрузится столько раз, сколько у вас фреймов).

  • Web Components (Кастомные элементы).
  • Каждая команда оборачивает свой микрофронтенд в стандартный Web Component (<catalog-widget>). Плюсы: Фреймворк-агностика (внутри может быть React, Vue или Angular), нативная поддержка браузерами. Минусы: Проблемы с Server-Side Rendering (SSR), сложности с передачей сложных структур данных через атрибуты DOM.

  • Module Federation (Стандарт индустрии).
  • Плагин, появившийся в Webpack 5 (и позже адаптированный для Vite и Rspack), который позволяет одному JavaScript-приложению динамически подгружать код из другого приложения в процессе выполнения.

    !Процесс загрузки Remote-модуля в Host-приложение через Module Federation

    В Module Federation есть два главных понятия: * Host (Shell) — контейнер приложения. Он загружается первым, отвечает за базовый роутинг и глобальный Layout (шапка, футер). * Remote — независимый микрофронтенд (например, страница оформления заказа), который развернут на своем собственном URL. Host запрашивает Remote только тогда, когда пользователь переходит на соответствующий маршрут.

    Главная суперсила Module Federation — Shared Dependencies (Общие зависимости). Вы можете настроить систему так, чтобы Host и Remote использовали один и тот же экземпляр React. Если Host уже загрузил react-dom, Remote не будет скачивать его заново, что радикально решает проблему раздувания бандла, свойственную iframe-подходу.

    Компромиссы (Trade-offs) микрофронтендов

    На Senior-интервью сказать «я выберу микрофронтенды, потому что это современно» — это красный флаг (негативный сигнал). Вы обязаны озвучить цену, которую система заплатит за это решение.

  • Сложность управления состоянием (State Management).
  • В монолите у вас есть единый Redux/Zustand store. В MFE каждый модуль изолирован. Если микрофронтенд «Каталог» добавляет товар, как микрофронтенд «Шапка» узнает, что нужно обновить счетчик корзины? Приходится строить шины событий (Event Bus) на базе CustomEvents или RxJS, что усложняет отладку.
  • Падение производительности.
  • Несмотря на Shared Dependencies, MFE часто приводит к каскадным загрузкам. Host загружается парсит роутинг понимает, что нужен Remote делает сетевой запрос за Remote Remote запрашивает свои данные. Это негативно влияет на метрики Web Vitals (в частности, на LCP).
  • Операционные накладные расходы.
  • Вместо одного CI/CD пайплайна вам нужно поддерживать десятки. Инфраструктура усложняется кратно.

    Как применять это на интервью

    Если задача звучит как: "Спроектируйте дашборд для внутренних операторов поддержки (B2B, 10 разработчиков в команде)", — не предлагайте микрофронтенды. Выбирайте SPA-монолит в монорепозитории. Обоснуйте это тем, что накладные расходы на инфраструктуру MFE превысят выгоду от независимых релизов для такой маленькой команды.

    Если задача звучит как: "Спроектируйте глобальный маркетплейс уровня Amazon (B2C, 500+ разработчиков, десятки независимых бизнес-юнитов)", — начинайте с микрофронтендов. Ваш алгоритм ответа:

  • Предложить разбить систему по бизнес-доменам (Поиск, Оформление заказа, Профиль).
  • Назначить каждый домен отдельной команде.
  • Выбрать Module Federation как механизм интеграции (Host-контейнер + Remote-страницы).
  • Озвучить риски: синхронизация стейта между доменами и дублирование зависимостей.
  • Архитектура — это искусство откладывать принятие решений до тех пор, пока они не станут абсолютно необходимыми. Монорепозиторий позволяет вам начать с монолита, навести порядок в коде и подготовить четкие границы модулей. И только когда релизный цикл начнет задыхаться от количества команд, эти модули можно будет безболезненно превратить в автономные микрофронтенды.

    4. Стратегии рендеринга и механизмы доставки приложения: обоснованный выбор между CSR, SSR и ISR

    Стратегии рендеринга и механизмы доставки приложения: обоснованный выбор между CSR, SSR и ISR

    В 2010 году рендеринг был простой задачей: сервер генерировал HTML, а браузер его показывал. Сегодня Senior-разработчик, выбирая между CSR, SSR и ISR, балансирует на канате, где задержка интерактивности страницы всего на 100 миллисекунд может стоить бизнесу миллионов долларов упущенной конверсии. Почему задача «просто показать интерфейс» превратилась в самый сложный архитектурный компромисс современного фронтенда?

    На System Design интервью от вас не ждут пересказа документации Next.js или React. Интервьюер хочет увидеть, как вы связываете бизнес-требования (SEO, частота обновления данных, стоимость инфраструктуры) с техническими метриками доставки контента.

    Анатомия доставки: конфликт видимости и интерактивности

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

  • TTFB (Time to First Byte) — время от отправки запроса до получения первого байта ответа.
  • FCP (First Contentful Paint) — время до отрисовки первого осмысленного контента (текста, картинки).
  • TTI (Time to Interactive) — время, когда страница становится полностью готовой к реакции на действия пользователя.
  • Главный конфликт рендеринга заключается в том, что улучшение FCP часто происходит за счет ухудшения TTI, и наоборот. Мостом между этими метриками в современных фреймворках выступает процесс гидратации.

    > Гидратация (Hydration) — это процесс, при котором клиентский JavaScript «оживляет» статический HTML, пришедший с сервера, воссоздавая виртуальный DOM и навешивая обработчики событий.

    !Процесс гидратации во времени

    Гидратация порождает эффект «зловещей долины» (Uncanny Valley) веб-разработки: пользователь видит кнопку, нажимает на неё, но ничего не происходит, потому что JavaScript еще не загрузился или не успел выполниться. Именно этот компромисс лежит в основе выбора архитектуры рендеринга.

    CSR (Client-Side Rendering): Наследие SPA

    При CSR сервер отдает минимальный пустой HTML-документ (обычно с одним <div>) и ссылку на большой JavaScript-бандл. Вся работа по запросу данных, построению DOM и маршрутизации происходит в браузере.

    Архитектурные компромиссы CSR:

  • Плюсы: Дешевая инфраструктура (статика отлично кэшируется на CDN), мгновенные переходы между страницами после первоначальной загрузки, богатые возможности управления сложным состоянием.
  • Минусы: Медленный FCP на слабых устройствах (пользователь видит белый экран, пока качается и парсится JS), огромные проблемы с SEO для поисковиков, не умеющих эффективно выполнять JavaScript.
  • Когда выбирать на интервью: B2B-дашборды, внутренние CRM-системы, сложные интерактивные веб-приложения (аналоги Figma, Notion или Google Docs). То есть там, где SEO не имеет значения, а сессия пользователя длится долго.

    SSR (Server-Side Rendering): Спасение SEO ценой сервера

    Чтобы решить проблему пустого экрана и SEO, архитектура смещается обратно на сервер. При SSR Node.js-сервер при каждом запросе обращается к базе данных или внутреннему API, рендерит React-компоненты в строку HTML и отправляет клиенту.

    !Сравнение архитектуры CSR, SSR и SSG

    Архитектурные компромиссы SSR:

  • Плюсы: Отличный SEO (поисковик сразу видит контент), быстрый FCP (пользователь видит интерфейс до загрузки JS).
  • Минусы: Высокий TTFB (сервер должен дождаться данных от API, прежде чем отдать HTML), дорогая инфраструктура (нужны мощные Node.js сервера), ярко выраженная проблема долгой гидратации.
  • Когда выбирать на интервью: Страницы, где данные уникальны для каждого пользователя, меняются ежесекундно, и при этом критически важно SEO. Однако в чистом виде классический SSR сегодня применяется редко из-за высокой нагрузки на сервер.

    SSG и ISR: Кэширование некэшируемого

    Если данные меняются не каждую секунду, генерировать HTML на каждый запрос — это пустая трата ресурсов.

    SSG (Static Site Generation) решает это радикально: HTML генерируется один раз на этапе сборки (Build Time). Идеально для блогов или документации, но неприменимо для крупных проектов. Если у вас маркетплейс на миллион товаров, сборка займет часы, а изменение цены одного товара потребует пересборки всего проекта.

    Здесь на сцену выходит ISR (Incremental Static Regeneration). ISR позволяет обновлять статические страницы в фоновом режиме, не прерывая работу пользователей и не требуя полной пересборки проекта.

    Математика инвалидации кэша в ISR опирается на простое условие:

    Где: * — временная метка текущего запроса от пользователя. * — временная метка момента, когда страница была сгенерирована и положена в кэш. * — заданный разработчиком интервал «свежести» (например, 60 секунд).

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

    Матрица принятия решений

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

    | Стратегия | SEO | Скорость FCP | Стоимость серверов | Актуальность данных | | :--- | :--- | :--- | :--- | :--- | | CSR | Плохо | Медленно | Низкая (CDN) | Высокая (запрос с клиента) | | SSR | Отлично | Быстро | Высокая (Node.js) | Абсолютная (на каждый запрос) | | SSG | Отлично | Мгновенно | Низкая (CDN) | Низкая (только при билде) | | ISR | Отлично | Мгновенно | Средняя | Контролируемая (задержка ) |

    Кульминация: Проектирование карточки товара (E-commerce)

    Давайте применим эти концепции к классической задаче Frontend System Design интервью: «Спроектируйте архитектуру рендеринга для страницы товара на Amazon/Ozon».

    Если вы скажете «Я выберу SSR, потому что нужно SEO», вы покажете уровень Middle. Senior-разработчик понимает, что страница товара неоднородна.

    Применим гибридный подход, разбивая требования:

  • Оболочка, описание, характеристики и отзывы: Эти данные меняются редко, но критичны для поисковиков. Мы используем ISR с равным, например, 10 минутам. Это обеспечит мгновенную загрузку из CDN и идеальное SEO.
  • Цена и наличие на складе: Эти данные меняются динамически, и продать товар по старой закэшированной цене — это бизнес-инцидент. Мы можем внедрить эти данные через CSR (показывать скелетон доли секунды поверх готового HTML) или использовать Edge SSR (выполнение легких функций на узлах CDN).
  • Корзина пользователя и статус авторизации: Это сугубо персонализированные данные. Их нельзя кэшировать на уровне CDN. Они всегда запрашиваются через CSR после монтирования компонента.
  • Таким образом, мы собрали цельную картину: мы не выбираем одну стратегию для всего приложения. Мы используем ISR для доставки тяжелого контента со скоростью статики, а CSR — для точечной инъекции динамического и пользовательского контекста после гидратации.

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

    5. Управление состоянием на уровне системы: синхронизация данных, оффлайн-режим и серверный стейт

    Управление состоянием на уровне системы: синхронизация данных, оффлайн-режим и серверный стейт

    Пользователь открывает ваш интернет-магазин в пяти разных вкладках. В одной из них он добавляет товар в корзину, переходит в другую вкладку, нажимает «Оформить заказ» — и получает ошибку, потому что корзина там пуста. Или другой сценарий: менеджер заполняет CRM-форму в поезде, интернет пропадает на секунду в момент нажатия кнопки «Сохранить», и час работы исчезает без следа. На уровне Senior-разработчика управление состоянием (State Management) перестает быть вопросом выбора между Redux и MobX. Оно превращается в задачу проектирования распределенной системы, где данные живут одновременно на сервере, в кэше браузера, в памяти UI-компонентов и в фоновых процессах.

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

    Великий раскол: Серверное и Клиентское состояние

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

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

    | Характеристика | Клиентское состояние (Client State) | Серверное состояние (Server State) | | :--- | :--- | :--- | | Владелец | Клиент (браузер) | Сервер (база данных) | | Жизненный цикл | Эфемерный (исчезает при закрытии вкладки) | Персистентный (хранится годами) | | Синхронность | Доступно мгновенно, синхронно | Требует асинхронной загрузки | | Актуальность | Всегда актуально для текущего UI | Может устареть (Stale) в любую миллисекунду | | Примеры | Текущая вкладка табов, текст в инпуте, тема оформления | Профиль пользователя, список товаров, баланс счета |

    Осознание этого раскола меняет архитектурный подход. Для серверного состояния мы больше не пишем экшены и редьюсеры. Мы используем паттерн Stale-while-revalidate (SWR). Клиент запрашивает данные, система мгновенно отдает устаревшую (stale) копию из локального кэша, параллельно в фоне делает запрос к серверу (revalidate) и незаметно обновляет UI свежими данными.

    > Архитектура фронтенда значительно упрощается, когда мы перестаем управлять серверными данными и начинаем управлять кэшем серверных данных.

    Синхронизация изолированных контекстов

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

    Синхронизация между вкладками (Cross-tab Sync)

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

    Для решения этой задачи в браузере существуют два основных механизма:

  • События LocalStorage (storage event). При изменении данных в localStorage браузер генерирует событие во всех других открытых вкладках того же домена. Это старый, но рабочий механизм. Минус — сериализация/десериализация JSON при каждой передаче и ограничение на объем (обычно около 5 МБ).
  • BroadcastChannel API. Современный стандарт для связи типа Pub/Sub (Издатель/Подписчик) между окнами, вкладками и iframe одного источника (origin). Он позволяет передавать любые JavaScript-объекты напрямую, без обращения к диску.
  • !Синхронизация вкладок через BroadcastChannel

    Архитектурно это выглядит так: при успешной мутации серверного состояния (например, товар добавлен в корзину и сервер ответил 200 OK), приложение отправляет сообщение в BroadcastChannel. Остальные вкладки, подписанные на этот канал, получают сигнал INVALIDATE_CART, сбрасывают свой локальный кэш и фоном запрашивают свежие данные.

    Глобальная шина событий в Микрофронтендах (Event Bus)

    В главе об архитектуре мы обсуждали микрофронтенды (MFE) и обещали разобрать, как они делят состояние. Главное правило MFE: микрофронтенды не должны иметь общего глобального стейта (например, общего Redux Store). Это создает жесткую связность (tight coupling) и ломает саму идею независимого развертывания.

    Вместо общего хранилища используется паттерн Event Bus (Шина событий). Host-приложение создает объект шины (по сути, реализацию паттерна Observer) и прокидывает его в Remote-модули. Когда модуль «Корзина» успешно завершает транзакцию, он не пытается изменить стейт модуля «Шапка сайта». Он просто публикует событие cart:item_added в Event Bus. Модуль «Шапка сайта», подписанный на это событие, реагирует на него и обновляет счетчик. Это гарантирует слабую связность: если модуль «Шапка сайта» упадет или будет удален, модуль «Корзина» продолжит работать.

    Архитектура Offline-First

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

    Для реализации такой архитектуры требуются два системных компонента: Service Worker и IndexedDB.

    !Архитектура Offline-first приложения

    Роль Service Worker (Сетевой прокси)

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

  • Отправить запрос в сеть.
  • Отдать закэшированный ответ.
  • Сохранить запрос в очередь для отложенной отправки.
  • Роль IndexedDB (Персистентное хранилище)

    Почему не localStorage? localStorage работает синхронно (блокирует главный поток UI) и вмещает мало данных. Для offline-first архитектуры используется IndexedDB — встроенная в браузер транзакционная NoSQL база данных. Она работает асинхронно, поддерживает индексы и может хранить сотни мегабайт данных (включая файлы и изображения).

    Жизненный цикл оффлайн-мутации

    Как спроектировать отправку сообщения в чате, если пользователь находится в туннеле?

  • Оптимистичный UI (уже знаком нам): Пользователь жмет «Отправить». UI мгновенно отрисовывает сообщение с пометкой «Отправляется...».
  • Запись в IndexedDB: Приложение сохраняет тело сообщения в локальную базу данных IndexedDB в таблицу outbox (исходящие).
  • Регистрация Background Sync: Приложение просит Service Worker зарегистрировать задачу фоновой синхронизации.
  • Восстановление сети: Как только устройство ловит сеть, браузер будит Service Worker (даже если вкладка с приложением уже закрыта).
  • Синхронизация: Service Worker читает таблицу outbox из IndexedDB, отправляет реальный POST-запрос на сервер. При успехе — удаляет запись из outbox.
  • Разрешение конфликтов (Conflict Resolution)

    Самая сложная часть оффлайн-режима — это момент, когда сеть возвращается. Представьте: пользователь редактировал документ на ноутбуке без сети (изменил заголовок). В это же время его коллега изменил текст этого же документа с телефона онлайн. Когда ноутбук подключится к сети, возникнет конфликт состояний.

    На интервью от Senior-разработчика ожидают понимания стратегий разрешения конфликтов. Выбор стратегии зависит от бизнес-требований.

    1. Last Write Wins (LWW) — Побеждает последняя запись

    Самый простой и частый подход. Каждая мутация снабжается временной меткой (timestamp). При конфликте сервер просто перезаписывает данные той мутацией, чья метка новее. Плюс: Легко реализовать. Минус: Возможна потеря данных (изменения "проигравшего" затираются навсегда). Подходит для профиля пользователя (обновление аватарки), но не для совместных документов.

    2. Ручное разрешение (Client-side Resolution)

    Сервер отказывается принимать конфликтующую мутацию и возвращает ошибку 409 Conflict вместе с текущей версией данных сервера. Клиентское приложение показывает пользователю оба варианта (Diff) и просит выбрать правильный. Пример: Разрешение merge-конфликтов в Git.

    3. CRDT (Conflict-free Replicated Data Types)

    Математический подход для совместного редактирования (Google Docs, Figma). Данные структурируются специальным образом (например, текст представляется не как строка, а как дерево символов с уникальными идентификаторами). Алгоритмы CRDT гарантируют, что независимо от порядка применения операций на разных клиентах, итоговое состояние сойдется к одному результату без вмешательства пользователя. Плюс: Идеальный UX для коллаборации. Минус: Огромная алгоритмическая сложность и увеличение объема передаваемых данных.

    Резюме

    Управление состоянием переросло рамки одного окна. Мы начали с разделения данных на серверные (кэш) и клиентские (UI). Затем связали изолированные вкладки и микрофронтенды через паттерны Pub/Sub (BroadcastChannel и Event Bus), чтобы избежать монолитного стейта. Наконец, мы спустились на уровень браузерного движка, внедрив Service Worker и IndexedDB, чтобы система оставалась работоспособной даже при полном обрыве связи, и определили правила разрешения конфликтов при ее восстановлении.

    В следующей главе мы рассмотрим, как именно эти данные путешествуют по сети: мы спроектируем сетевой слой и сделаем обоснованный выбор между REST, GraphQL и протоколами реального времени.

    6. Проектирование сетевого слоя и API: выбор протоколов и контрактов взаимодействия

    Проектирование сетевого слоя и API: выбор протоколов и контрактов взаимодействия

    Представьте, что вы спроектировали идеальную архитектуру клиента: компоненты изолированы, состояние нормализовано, рендеринг оптимизирован. Но при загрузке дашборда приложение делает 45 последовательных HTTP-запросов, а график «реального времени» обновляется с заметными рывками, потребляя мегабайты трафика. Пользовательский опыт разрушен. На System Design интервью умение рисовать квадратики компонентов — это лишь база. Уровень Senior проверяется тем, как именно эти квадратики общаются с сервером. Сетевой слой — это кровеносная система вашего приложения, и выбор неправильного протокола способен убить любую, даже самую изящную архитектуру.

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

    Контракты взаимодействия: как мы запрашиваем данные

    Исторически фронтенд подстраивался под то, что отдает бэкенд. Сегодня архитектура API диктуется потребностями клиентских приложений. Выбор между REST, GraphQL и RPC — это всегда компромисс (trade-off) между кэшируемостью, размером полезной нагрузки (payload) и связностью команд.

    REST: Стандарт и предсказуемость

    REST оперирует ресурсами. Если нам нужен профиль пользователя, мы обращаемся к /users/123.

    Сильные стороны:

  • Идеальная совместимость с HTTP-инфраструктурой. GET-запросы легко кэшируются на уровне CDN (что мы детально разберем в будущих главах).
  • Независимость команд (бэкенд может менять внутреннюю реализацию, не ломая контракт).
  • Слабые стороны (почему появился GraphQL):

  • Overfetching (избыточная выборка): Сервер отдает весь объект пользователя, даже если для UI нужен только аватар.
  • Underfetching (недостаточная выборка): Чтобы отрендерить страницу поста с комментариями и авторами комментариев, клиенту приходится делать каскад (waterfall) запросов: сначала пост, затем список комментариев, затем профили их авторов.
  • GraphQL: Клиентоцентричность

    В GraphQL фронтенд сам декларирует, какая структура данных ему нужна в данный момент, отправляя запрос на единый эндпоинт (обычно /graphql).

    > GraphQL переносит ответственность за формирование ответа с сервера на клиента. Это решает проблемы избыточной и недостаточной выборки, но ценой усложнения серверной инфраструктуры и потери бесплатного HTTP-кэширования. > > Из документации GraphQL

    RPC (Remote Procedure Call): Действия вместо ресурсов

    В парадигмах вроде gRPC-Web или tRPC мы вызываем функции сервера так, будто они находятся локально на клиенте (например, api.users.banUser(id)). Это обеспечивает строгую типизацию от базы данных до UI, но создает сильную связность (coupling) между клиентом и сервером. RPC отлично подходит для монорепозиториев, где фронтенд и бэкенд пишутся на одном языке (TypeScript) и деплоятся синхронно.

    Сравнительная таблица контрактов

    | Критерий | REST | GraphQL | RPC (tRPC / gRPC) | | :--- | :--- | :--- | :--- | | Парадигма | Ресурсы (Существительные) | Граф данных | Действия (Глаголы) | | Кэширование (CDN/Браузер) | Отличное (на базе URL) | Сложное (все запросы — POST) | Зависит от транспорта | | Проблема Over/Underfetching | Высокая | Решена полностью | Зависит от реализации эндпоинта | | Типизация из коробки | Требует OpenAPI/Swagger | Встроена (Схема) | Встроена (Сквозная) |

    Парадигмы реального времени: от Polling к WebSockets

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

    Polling (Опрос)

    Самый примитивный подход: клиент запрашивает данные по таймеру каждые секунд. Главная проблема — инфраструктурный оверхед (накладные расходы). Каждый HTTP-запрос несет с собой заголовки.

    Оценим объем паразитного трафика с помощью формулы:

    Где:

  • — общий объем накладных расходов (в байтах).
  • — размер заголовков запроса (включая Cookie).
  • — размер заголовков ответа.
  • — длительность сессии пользователя.
  • — интервал опроса.
  • Если заголовки весят 1 КБ, а опрос идет каждую секунду в течение часа, один клиент генерирует около 3.6 МБ только на заголовки, даже если данные не изменились. На масштабе в миллион пользователей это убьет сеть.

    Server-Sent Events (SSE)

    SSE — это стандартный браузерный API, работающий поверх обычного HTTP. Клиент открывает постоянное соединение, и сервер пушит текстовые события.

    Ключевое отличие: Соединение однонаправленное (Server Client). Это идеальный выбор для ленты новостей, уведомлений или обновления статуса CI/CD пайплайна. SSE поддерживает автоматическое переподключение из коробки и не требует сложного балансировщика нагрузки, в отличие от WebSockets.

    WebSockets (WS)

    Протокол, обеспечивающий полнодуплексное (двунаправленное) постоянное соединение.

  • Плюсы: Минимальный оверхед на сообщение (всего несколько байт фрейма вместо килобайтов HTTP-заголовков). Идеально для многопользовательских игр, совместного редактирования (где мы применяем CRDT из предыдущей главы) и высокочастотного трейдинга.
  • Минусы: Соединение имеет состояние (stateful). Если сервер падает, клиент должен переподключиться. В распределенных системах это требует применения паттерна Pub/Sub (например, через Redis) на бэкенде, чтобы сообщение от одного инстанса сервера дошло до клиента, подключенного к другому инстансу.
  • Передача больших объемов данных: стратегии пагинации

    На интервью часто просят спроектировать ленту (Feed) или таблицу с миллионами записей. Загрузить всё сразу невозможно. Выбор метода пагинации критически влияет на стабильность системы.

    Offset-based пагинация (Смещение)

    Классический подход с параметрами limit и offset (или page). Проблема: Нестабильность данных (Data Drift). Если между запросом первой и второй страницы в базу добавится новый элемент, все остальные сдвинутся. Пользователь увидит дубликат элемента на второй странице. Кроме того, производительность базы данных падает линейно: сложность поиска составляет , где — размер смещения, так как БД должна просканировать и отбросить все предыдущие строки.

    Cursor-based пагинация (Курсорная)

    Вместо смещения клиент передает уникальный идентификатор (курсор) последнего полученного элемента. Запрос выглядит как «дай мне 20 элементов, которые идут после элемента с ID 1543».

  • Сложность: при наличии индекса в БД.
  • Стабильность: Вставка новых элементов не сдвигает курсор, дубликаты исключены.
  • Именно курсорная пагинация является стандартом для бесконечных лент (Infinite Scroll) в социальных сетях.

    Оптимизация на уровне клиента: Batching и Deduplication

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

    Для решения этой проблемы на клиенте реализуется сетевой шлюз (часто встроенный в библиотеки Server State):

  • Deduplication (Дедупликация): Если компонент A и компонент B одновременно запрашивают /users/1, сетевой слой отправляет только один физический HTTP-запрос, а ответ возвращает обоим компонентам.
  • Batching (Пакетирование): Сбор множества мелких запросов в один большой в рамках короткого временного окна (например, 50 мс). 10 запросов вида GET /users/id превращаются в один GET /users?ids=1,2,3....
  • > Важное правило: Операции мутации (POST/PUT/DELETE) должны обладать свойством идемпотентности — повторный вызов операции не должен менять состояние системы сверх того, что было сделано при первом вызове. Это позволяет безопасно реализовывать автоматические повторы (retries) при обрывах сети.

    Синтез знаний: Проектирование сети для торгового терминала

    Сведем изученное воедино на примере классической задачи с интервью: «Спроектируйте архитектуру финансового B2B-терминала».

    Как Senior-разработчик, вы не выбираете один протокол. Вы комбинируете их под разные нефункциональные требования:

  • Инициализация и профиль пользователя (REST):
  • При старте приложения мы запрашиваем настройки пользователя, права доступа и баланс через REST. Эти данные меняются редко, их можно закэшировать, а REST обеспечит надежность и простоту.
  • Исторические графики и аналитика (GraphQL):
  • Пользователь строит сложные отчеты, комбинируя акции, облигации и периоды. GraphQL позволит UI-компонентам запрашивать ровно ту глубину данных, которая нужна для конкретного виджета, избегая Overfetching.
  • Поток котировок (SSE):
  • Цены на активы обновляются десятки раз в секунду. Данные идут только в одну сторону (от биржи к клиенту). Использование WebSockets здесь избыточно, так как нам не нужно отправлять данные обратно с той же частотой. SSE идеально справится с однонаправленным потоком.
  • Исполнение ордеров на покупку (REST/RPC с идемпотентностью):
  • Отправка приказа на покупку — это критическая транзакция. Мы используем обычный HTTP POST, но обязательно генерируем на клиенте уникальный Idempotency-Key (например, UUID). Если сеть моргнет и клиент отправит запрос дважды, бэкенд по ключу поймет, что это дубль, и не спишет деньги (USD) дважды.

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

    7. Глобальная оптимизация производительности и управление метриками Web Vitals

    Глобальная оптимизация производительности и управление метриками Web Vitals

    Парадокс современного фронтенда: приложение может иметь идеальную архитектуру, 100% покрытие тестами и использовать самые современные фреймворки, но для пользователя оно будет ощущаться как «медленное и сломанное». На System Design интервью кандидаты часто совершают фатальную ошибку, сводя разговор о производительности к локальным оптимизациям вроде мемоизации компонентов или дебаунсинга инпутов. Однако на уровне Senior-разработчика оптимизация — это не борьба с лишними рендерами в React. Это проектирование системы доставки, рендеринга и исполнения кода, которая гарантирует предсказуемый пользовательский опыт в условиях нестабильной сети и слабых устройств.

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

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

    Исторически производительность измерялась техническими событиями браузера: window.onload или DOMContentLoaded. Проблема в том, что эти события никак не коррелируют с тем, что видит пользователь. Страница может загрузить весь HTML, но оставаться белым экраном из-за блокирующего JavaScript.

    Google стандартизировал подход к оценке производительности через набор метрик Core Web Vitals (CWV). Они оценивают три столпа пользовательского опыта: скорость загрузки, визуальную стабильность и отзывчивость.

    | Метрика | Расшифровка | Что измеряет | Целевое значение | | :--- | :--- | :--- | :--- | | LCP | Largest Contentful Paint | Время до отрисовки самого большого видимого элемента в области просмотра (картинка, видеоблок, крупный текст). | сек. | | CLS | Cumulative Layout Shift | Сумма всех неожиданных смещений контента во время жизни страницы. | | | INP | Interaction to Next Paint | Задержка реакции интерфейса на действия пользователя (клики, тапы, нажатия клавиш) на протяжении всей сессии. | мс. |

    > «Оптимизация производительности — это не про то, как сделать код быстрее. Это про то, как сделать так, чтобы пользователь как можно раньше поверил, что приложение работает». > > Илья Григорик, веб-инженер и автор книг по производительности

    LCP: Управление критическим путем рендеринга

    LCP — это не просто метрика скорости, это индикатор того, насколько эффективно вы спроектировали Критический путь рендеринга (Critical Rendering Path). На System Design интервью, если вы проектируете лендинг, маркетплейс или медиа-ресурс, интервьюер обязательно спросит, как вы обеспечите быстрый LCP.

    Время LCP математически складывается из четырех фаз:

    Где:

  • — время ответа сервера (мы обсуждали влияние стратегий SSR/ISR на него в Главе 4).
  • — задержка перед началом загрузки ресурса LCP (например, браузер не начнет качать картинку, пока не распарсит HTML и не найдет тег <img>).
  • — время скачивания самого ресурса по сети.
  • — время от завершения загрузки до фактической отрисовки (может блокироваться выполнением JS).
  • Архитектурные паттерны оптимизации LCP

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

  • Resource Hints (Подсказки браузеру)
  • Вместо того чтобы ждать, пока браузер сам обнаружит тяжелый Hero-баннер или критический веб-шрифт, мы декларируем их заранее в заголовках документа. - preload — немедленная загрузка критического ресурса с высоким приоритетом (например, главной картинки статьи). - preconnect — заблаговременное установление сетевого соединения (DNS, TCP, TLS) с внешним доменом, откуда будут грузиться данные (например, с CDN).

  • Оптимизация доставки медиа (Image/Video Architecture)
  • Если LCP-элемент — это изображение, архитектура должна подразумевать использование специализированных CDN для трансформации изображений на лету. Сервер определяет устройство клиента (через заголовки Accept и User-Agent) и отдает WebP/AVIF нужного разрешения, снижая .

  • Исключение зависимости от клиентского стейта
  • Антипаттерн: рендерить LCP-элемент только после получения данных от API на клиенте (CSR). В этом случае LCP жестко привязан к завершению цепочки: загрузка HTML загрузка JS выполнение JS запрос к API рендер. Решение: для страниц, где важен LCP (например, карточка товара), критический контент должен отдаваться сразу в HTML (через SSR или ISR).

    CLS: Проектирование визуальной предсказуемости

    Cumulative Layout Shift измеряет визуальную стабильность. Высокий CLS разрушает пользовательский опыт: человек хочет кликнуть на кнопку «Отмена», но в этот момент сверху подгружается рекламный баннер, контент съезжает, и происходит клик по кнопке «Купить».

    Причины CLS и системные решения

  • Изображения и iframe без заданных размеров. Браузер не знает, сколько места займет картинка, пока не скачает ее заголовки.
  • Решение: Резервирование пространства через CSS-свойство aspect-ratio или явное указание width и height.
  • Динамический инжект контента. Подгрузка рекомендаций или уведомлений над основным контентом.
  • Решение: Проектирование UI с использованием паттерна Skeleton Screens (скелетные загрузки). Место под динамический блок резервируется еще на этапе серверного рендеринга или первой отрисовки.
  • Мигание шрифтов (FOUT/FOIT). Текст рендерится системным шрифтом, а затем перерисовывается кастомным, меняя ширину строк и высоту блоков.
  • Решение: Использование дескриптора font-display: optional (если шрифт не успел загрузиться сразу, используется системный, и замены не происходит до следующей сессии) или тонкая настройка size-adjust для выравнивания метрик системного и кастомного шрифтов.

    INP: Освобождение главного потока

    В 2024 году метрика INP официально заменила FID (First Input Delay). Если FID измерял задержку только первого взаимодействия, то INP оценивает все клики и вводы текста за время жизни страницы и выбирает худший результат.

    Браузер использует один главный поток (Main Thread) для выполнения JavaScript, вычисления стилей, создания слоев и отрисовки (Paint). Если JavaScript выполняет тяжелую задачу, поток блокируется.

    Long Task (Долгая задача) — это любая задача в главном потоке, выполнение которой занимает мс. Если пользователь кликнет по кнопке на 10-й миллисекунде выполнения такой задачи, интерфейс не отреагирует еще минимум 40 мс.

    Стратегии борьбы с блокировкой потока

    На уровне архитектуры мы не можем просто «писать код быстрее». Мы должны управлять распределением вычислений:

  • Yielding to Main Thread (Уступка потока)
  • Разбиение монолитных синхронных вычислений на микро-задачи. Если системе нужно обработать массив из 100 000 элементов, мы обрабатываем их чанками по 1000, используя setTimeout или scheduler.yield(), возвращая контроль браузеру между итерациями, чтобы он мог отрисовать реакцию на клик пользователя.

  • Web Workers для тяжелых вычислений
  • Вынос бизнес-логики за пределы главного потока. Пример: В веб-версии Figma или при парсинге тяжелого CSV-файла на клиенте, математические расчеты и обработка данных происходят в фоновом потоке (Web Worker). Главный поток занимается исключительно приемом сообщений от воркера и обновлением DOM.

  • Interaction-based Code Splitting
  • Вместо того чтобы загружать весь код компонента при старте страницы, мы загружаем его только в момент намерения пользователя. Например, код тяжелого виджета чата поддержки скачивается не при загрузке страницы, а по событию mouseenter на иконку чата.

    Паттерн PRPL: Комплексный подход к доставке

    Для структурирования ответа на интервью полезно использовать высокоуровневые концепции. Одной из таких является паттерн PRPL, разработанный инженерами Google. Он описывает идеальный жизненный цикл доставки приложения:

  • Push (или Preload) — предварительная загрузка критических ресурсов для первоначального маршрута (использование Resource Hints).
  • Render — максимально быстрый рендеринг первоначального маршрута (использование SSR/ISR для LCP).
  • Pre-cache — фоновое кэширование ресурсов для остальных маршрутов (через Service Worker, который мы обсуждали в Главе 5).
  • Lazy-load — ленивая загрузка некритических ресурсов и кода по мере необходимости.
  • Применяя PRPL, вы показываете интервьюеру, что мыслите не категориями отдельных фич, а жизненным циклом всего приложения: от первого байта до оффлайн-навигации.

    Резюме для интервью

    Когда на секции System Design вас просят спроектировать высоконагруженный клиентский интерфейс, ваш ответ по оптимизации должен строиться сверху вниз:

  • Определите критические метрики для бизнеса. Для e-commerce важен LCP (влияет на конверсию). Для текстового редактора или дашборда — INP (влияет на ощущение отзывчивости).
  • Спроектируйте доставку LCP-элемента. Объясните, как выстроить цепочку от CDN до браузера без блокирующих запросов.
  • Защитите визуальную стабильность (CLS). Упомяните резервирование пространства и скелетоны для асинхронных данных.
  • Обеспечьте отзывчивость (INP). Предложите вынос тяжелой логики в Web Workers или разбиение задач.
  • Архитектура, ориентированная на Web Vitals — это фундамент. Однако для обеспечения этой производительности в реальном мире, где пользователи находятся на разных континентах, нам потребуется надежная инфраструктура доставки. О том, как спроектировать сеть CDN и настроить процессы CI/CD для фронтенда, мы поговорим в следующей главе.

    8. Инфраструктура фронтенда: CI/CD пайплайны, CDN и стратегии развертывания

    Инфраструктура фронтенда: CI/CD пайплайны, CDN и стратегии развертывания

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

    CI/CD пайплайн: от исходного кода до артефакта

    Непрерывная интеграция и доставка (CI/CD) для крупных фронтенд-приложений кардинально отличается от базового сценария «собрать бандл и закинуть на сервер». В масштабных системах, особенно использующих монорепозитории, пайплайн становится узким местом разработки.

    Главный архитектурный принцип современного CI/CD — неизменяемость артефактов (Immutable Artifacts). Сборка приложения (npm run build) должна происходить ровно один раз. Полученный бандл продвигается по средам (Dev Staging Prod) без пересборки. Конфигурация, специфичная для среды (URL API, ключи аналитики), внедряется не на этапе компиляции (build-time), а на этапе выполнения (run-time) — например, через глобальный объект window.ENV, который генерируется сервером при отдаче index.html.

    В условиях монорепозиториев пайплайн требует интеллектуального кэширования. Инструменты вроде Nx или Turborepo используют хэширование на основе содержимого (content-addressable hashing). Пайплайн вычисляет хэш для каждого пакета на основе его исходного кода, зависимостей и переменных окружения. Если хэш совпадает с предыдущей сборкой, CI пропускает этапы линтинга, тестирования и компиляции, извлекая готовый артефакт из распределенного кэша. Это снижает время CI с десятков минут до секунд.

    Географическая доставка: CDN и Edge Computing

    Собранный артефакт необходимо доставить пользователю. Физику обмануть нельзя: время передачи сигнала ограничено скоростью света в оптоволокне. Задержка сети (Latency) напрямую зависит от расстояния: , где — расстояние между клиентом и сервером, а — скорость распространения сигнала (около м/с в кабеле). Если ваш сервер находится во Франкфурте, а пользователь — в Сиднее, только базовый прием-передача пакета (RTT) займет сотни миллисекунд, разрушая метрики доставки.

    Для решения этой проблемы используется CDN (Content Delivery Network) — распределенная сеть серверов, кэширующая статику максимально близко к пользователю.

    Механика работы CDN

    CDN опирается на сеть узлов PoP (Point of Presence), расположенных в дата-центрах по всему миру. Когда клиент запрашивает app.js, запрос не идет на ваш основной сервер (Origin). Благодаря протоколу маршрутизации Anycast, сетевой запрос автоматически направляется к топологически ближайшему узлу PoP.

    Если файл есть в кэше узла (Cache Hit), он отдается мгновенно. Если нет (Cache Miss) — узел запрашивает его у Origin-сервера, сохраняет у себя и отдает клиенту. Эффективность CDN измеряется коэффициентом попадания в кэш:

    где (Cache Hit Ratio) — доля успешных запросов к кэшу, — количество попаданий (Hits), — количество промахов (Misses).

    Edge Computing: вычисления на границе сети

    Современные CDN эволюционировали из простых файловых хранилищ в платформы граничных вычислений (Edge Computing). Технологии вроде Cloudflare Workers или AWS Lambda@Edge позволяют выполнять легковесный JavaScript-код прямо на узлах PoP, в миллисекундах от пользователя.

    Edge-функции выполняются в изолированных средах (V8 Isolates), что обеспечивает старт за доли миллисекунды. Это открывает мощные архитектурные возможности:

  • A/B тестирование без мерцания: Edge-функция анализирует cookie пользователя и отдает нужную версию index.html до того, как ответ достигнет браузера, исключая влияние на метрику CLS.
  • Персонализация и локализация: Модификация HTTP-заголовков или внедрение переводов на лету на основе IP-адреса.
  • Edge SSR: Рендеринг React-компонентов на границе сети, что снижает нагрузку на центральные сервера и ускоряет отдачу динамического контента.
  • Стратегии безопасного развертывания

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

    > Деплой (развертывание) — это технический процесс установки кода на сервер. Релиз — это бизнес-решение сделать функционал доступным пользователям. В зрелой архитектуре эти процессы полностью разделены. > > Мартин Фаулер, "Feature Toggles"

    Для минимизации рисков применяются продвинутые стратегии развертывания.

    | Стратегия | Механика | Плюсы | Минусы | | :--- | :--- | :--- | :--- | | Blue-Green Deployment | Поднимаются две идентичные среды (Синяя — текущая, Зеленая — новая). Трафик переключается на Зеленую через балансировщик мгновенно. | Нулевое время простоя. Мгновенный откат (переключение роутера обратно). | Требует двойных инфраструктурных мощностей. | | Canary Release | Новая версия выкатывается на небольшую долю пользователей (например, 5%). При стабильности метрик доля постепенно увеличивается до 100%. | Раннее обнаружение аномалий на реальном трафике без риска для всей аудитории. | Сложность настройки балансировщика и мониторинга версий. | | Feature Flags | Код развертывается отключенным. Функционал включается через удаленный конфигурационный сервис (LaunchDarkly, Firebase) по условиям. | Позволяет тестировать фичи в продакшене, делать таргетированные релизы (только для бета-тестеров). | Усложняет код (множество if/else), требует регулярной очистки старых флагов. |

    Инфраструктура микрофронтендов: Манифесты и инвалидация кэша

    Микрофронтенды усложняют пайплайн, так как их главная цель — независимое развертывание (Independent Deployment). Команда Корзины должна выкатить обновление, не затрагивая команду Каталога, и без пересборки всего приложения.

    В архитектуре Module Federation это решается через паттерн динамических манифестов. При сборке микрофронтенда (Remote) генерируется файл-указатель — remoteEntry.js. Этот файл содержит карту актуальных хэшированных чанков.

    Когда Host-приложение загружается в браузере, оно запрашивает remoteEntry.js микрофронтенда. Чтобы система работала корректно, инфраструктура CDN должна быть настроена с двумя противоположными правилами кэширования:

  • Чанки кода ([hash].js): Кэшируются навсегда (Cache-Control: max-age=31536000, immutable). Они никогда не меняются — новая сборка создаст файлы с новыми хэшами.
  • Манифест (remoteEntry.js): Не кэшируется вообще или ревалидируется при каждом запросе (Cache-Control: no-cache).
  • Если remoteEntry.js застрянет в кэше CDN, Host-приложение будет пытаться скачать старые чанки, которые могли быть уже удалены с Origin-сервера при очистке, что приведет к фатальной ошибке загрузки модуля.

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

    9. Безопасность, мониторинг и логирование: защита системы и контроль стабильности

    Безопасность, мониторинг и логирование: защита системы и контроль стабильности

    В 2014 году безобидный на первый взгляд твит, содержащий специфическую последовательность символов, заставил тысячи пользователей незаметно для себя ретвитнуть его, распространив XSS-червя по всему Twitter за считанные минуты. Ваша идеально спроектированная микрофронтенд-архитектура, доставляемая через Edge-серверы за миллисекунды, не имеет никакого значения, если злоумышленник может выполнить произвольный JavaScript в браузере пользователя, или если приложение молча падает у 10% аудитории, а вы узнаете об этом только из гневных отзывов в App Store.

    На уровне Senior-разработчика безопасность и наблюдаемость (Observability) системы — это не патчи, которые наклеиваются поверх готового кода. Это архитектурные слои, которые закладываются на этапе проектирования сетевых контрактов и CI/CD пайплайнов.

    Безопасность как архитектурный слой

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

    XSS (Cross-Site Scripting) и Content Security Policy

    XSS возникает, когда приложение рендерит недоверенные данные как исполняемый код. Хотя современные фреймворки (React, Angular) экранируют текстовые данные по умолчанию, уязвимости проникают через прямые манипуляции с DOM (например, dangerouslySetInnerHTML), сторонние виджеты или скомпрометированные NPM-пакеты.

    Главный архитектурный ответ на угрозу XSS — это CSP (Content Security Policy).

    > Content Security Policy — это дополнительный уровень безопасности, который позволяет владельцам веб-сайтов декларировать утвержденные источники контента, разрешенные к загрузке и исполнению в браузере.

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

    Для современных SPA-приложений базовый подход с белыми списками доменов устарел. Senior-инженеры проектируют CSP на основе nonce (одноразовых чисел) или криптографических хэшей:

  • Сервер при рендеринге страницы генерирует уникальный криптографически стойкий токен (nonce).
  • Этот токен добавляется в заголовок: Content-Security-Policy: script-src 'nonce-r4nd0m' 'strict-dynamic';
  • Тот же токен прописывается в теге скрипта: <script nonce="r4nd0m" src="/app.js"></script>
  • Директива 'strict-dynamic' позволяет доверенному скрипту (нашему app.js) динамически загружать другие чанки (что критично для ленивой загрузки и микрофронтендов), сохраняя при этом защиту от инъекций в HTML.

    CSRF (Cross-Site Request Forgery) и эволюция Cookies

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

    Исторически для защиты использовались Anti-CSRF токены, которые фронтенд должен был читать из мета-тегов и прикреплять к каждому мутирующему запросу. Сегодня архитектурный стандарт защиты сместился на уровень управления Cookies через атрибут SameSite.

    | Значение SameSite | Поведение браузера | Архитектурное применение | | :--- | :--- | :--- | | Strict | Cookie отправляются только если запрос инициирован с того же домена. | Финансовые операции, смена пароля. Максимальная защита, но ломает UX при переходах по внешним ссылкам. | | Lax | Cookie отправляются при навигации верхнего уровня (переход по ссылке), но блокируются для кросс-доменных POST/PUT запросов. | Стандарт по умолчанию для большинства современных веб-приложений. | | None | Cookie отправляются всегда. Требует обязательного флага Secure (только HTTPS). | Необходим для виджетов, iframe и микрофронтендов, интегрируемых на сторонние ресурсы. Требует возврата к Anti-CSRF токенам. |

    CORS: Защита пользователя, а не сервера

    Одно из самых частых заблуждений на интервью — попытка использовать CORS (Cross-Origin Resource Sharing) для защиты API от DDoS или парсинга.

    > CORS — это механизм браузера, а не сервера. Он защищает пользователя от того, чтобы вредоносный сайт не смог прочитать приватные данные из API другого сервиса от имени этого пользователя. > > MDN Web Docs

    Если фронтенд делает сложный кросс-доменный запрос (например, с кастомными заголовками вроде Authorization или методом PUT), браузер автоматически отправляет Preflight-запрос методом OPTIONS. Сервер должен ответить, разрешает ли он такую операцию для данного домена.

    Архитектурный компромисс: Preflight-запросы удваивают сетевую задержку. Для оптимизации Senior-разработчик закладывает кэширование CORS-ответов на уровне API Gateway или CDN с помощью заголовка Access-Control-Max-Age, снижая накладные расходы на сеть.

    Наблюдаемость (Observability): Мониторинг и обработка ошибок

    Безопасность предотвращает инциденты, а наблюдаемость позволяет узнать о них, когда защита пробита или код дал сбой. console.log бесполезен, когда приложение работает на устройствах миллионов пользователей.

    Глобальный перехват и Error Boundaries

    Надежная система мониторинга строится на тотальном перехвате исключений. На уровне архитектуры мы внедряем три слоя:

  • Глобальные обработчики: window.onerror для синхронных ошибок и window.onunhandledrejection для упавших промисов (критично для сетевого слоя).
  • Error Boundaries (Границы ошибок): В React это компоненты-обертки, которые локализуют падение рендера. Архитектурно мы не оборачиваем всё приложение в один Boundary (иначе падение виджета сломает весь интерфейс). Мы изолируем независимые бизнес-блоки (например, сайдбар, ленту, корзину).
  • Трекинг-клиент (например, Sentry): Агрегирует ошибки, добавляет контекст (версия браузера, ОС, действия пользователя) и отправляет на сервер.
  • Безопасная работа с Source Maps

    Минифицированный код в продакшене делает стектрейсы ошибок нечитаемыми (TypeError: c is not a function at x.js:1:42). Для их расшифровки нужны Source Maps. Однако публикация Source Maps в открытом доступе раскрывает исходный код приложения, что является риском безопасности.

    Правильный архитектурный паттерн интегрируется в CI/CD пайплайн:

  • На этапе сборки (CI) генерируются Source Maps.
  • Они загружаются напрямую в систему мониторинга (Sentry) через API с привязкой к хэшу релиза.
  • Из финального артефакта, который отправляется на CDN, файлы .map удаляются.
  • В результате пользователи получают легкий и защищенный код, а разработчики в панели мониторинга видят оригинальный стектрейс.

    Телеметрия и распределенная трассировка

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

    Стратегия сэмплирования (Sampling)

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

    Определим объем генерируемых логов в системе:

    Где — общий объем логов (байт/с), — количество активных пользователей, — частота событий на пользователя (событий/с), а — средний размер одного события (байт).

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

    Например, для сбора Core Web Vitals (INP, LCP) у 10 миллионов пользователей достаточно установить (1%), чтобы получить статистически значимую картину без перегрузки серверов телеметрии.

    Distributed Tracing (Распределенная трассировка)

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

  • При старте сессии или критическом действии (например, нажатие "Оплатить") фронтенд генерирует уникальный идентификатор (UUID).
  • Этот идентификатор прикрепляется ко всем исходящим HTTP-запросам в виде заголовка (например, X-Request-ID или traceparent).
  • API Gateway принимает запрос и передает этот же ID всем внутренним микросервисам.
  • Любая ошибка или лог на сервере помечается этим ID.
  • Если пользователь жалуется на сбой оплаты, служба поддержки запрашивает у него его сессионный ID. Разработчик вбивает этот ID в систему логирования (Kibana/Datadog) и видит всю цепочку: от клика в браузере и параметров стейта до конкретного SQL-запроса в базе данных, который вызвал таймаут.

    Синтез: Когда всё работает вместе

    Представьте ситуацию: пользователь сообщает о белом экране при открытии профиля.

    Благодаря архитектуре наблюдаемости, мы не гадаем. Мы открываем дашборд и видим:

  • Sentry поймал ошибку через Error Boundary компонента UserProfile. Стектрейс, расшифрованный через скрытые Source Maps, указывает на сбой парсинга данных.
  • По Correlation ID, прикрепленному к отчету об ошибке, мы находим логи бэкенда. Видим, что API вернул строку с внедренным тегом <script>, которую кто-то сохранил в поле "О себе".
  • Логи CSP (Content Security Policy) показывают, что браузер заблокировал выполнение этого скрипта, предотвратив кражу сессионных SameSite cookies.
  • Из-за блокировки скрипта нарушилась структура данных, что вызвало ошибку рендера, перехваченную Error Boundary.
  • Система защитила пользователя от XSS-атаки, локализовала падение интерфейса (остальное приложение продолжило работать) и предоставила разработчикам исчерпывающий контекст для исправления уязвимости на бэкенде. Это и есть результат работы Senior-инженера: система, которая не просто работает, но умеет защищаться и рассказывать о своих проблемах. В следующей главе мы сведем все изученные концепции воедино и разберем сквозной пример прохождения реального интервью от сбора требований до финальной архитектурной схемы.