Как инженерия фронтенда эволюционировала от стилизации до масштабирования систем
Инженерия фронтенда развивалась от HTML и CSS до компонентов React, кэширования, инструментов для управления монорепозиториями и механизмов отслеживания состояния. Что менялось на каждом этапе и почему это происходило.
Источники: I Thought Frontend Was Colors and Fonts. Then I Shipped to a Million Users.
Инструменты развивались, но наше мышление изменилось ещё сильнее.
Если спросить кого-нибудь, что означает разработка фронтенда, вы обычно услышите один и тот же ответ: HTML, CSS, JavaScript. Цвета, шрифты, изображения, макет.
Это разумная отправная точка. Но на этом история не заканчивается.
То, что начиналось с создания веб-страниц, превратилось в разработку полноценных систем, которые должны оставаться быстрыми, корректными и отслеживаемыми при одновременной работе с ними тысяч людей.
Инструменты развивались в соответствии с этим направлением. Но ещё больше изменился набор вопросов, которые мы научились себе задавать.
Вот полный путь развития.
1. Всё начиналось с HTML, CSS и JavaScript
Ранний веб сохранял всё простым.
HTML отвечал за структуру. CSS — за визуальное оформление. JavaScript добавлял некоторую интерактивность.
Задача сводилась к одному предложению:
Создать страницу, которая хорошо выглядит и корректно функционирует.
Большинство страниц были статическими. Серверы передавали полный HTML-код, браузери его отображали, причем сами браузеры практически не содержали логики.
Со временем страницы стали выполнять больше функций. Формы начали проверять введенные данные по мере их ввода. Контент обновлялся без полной перезагрузки страницы.
JavaScript продолжал брать на себя все больше обязанностей и никогда не передавал их кому-либо другому.
- От страниц к приложениям
Веб-сайты превратились в приложения. Это означало появление динамических интерфейсов, вызовов API, форм с несколькими шагами, процедур аутентификации и хранение данных в браузере между отдельными взаимодействиями.
Это изменение выявило проблему, которую старые методы не были созданы для решения.
Когда вы работаете непосредственно с DOM, видимый экран становится единственным источником правды. Одна и та же информация может находиться в переменной, внутри узла DOM и в ответе от сервера. Ничто не гарантирует, что эти три источника будут согласованы.
Если обновить только один из них, забыв о остальных, это приведет к ошибке, которая проявится лишь при определенной последовательности действий.
Потребовался способ описания того, как должен выглядеть интерфейс, и передачи задачи по определению конкретных операций с DOM кому-то еще.
3. React: мышление в компонентах
Настоящим прорывом React стала не его синтаксис, а лежащая в основе модель мышления.
UI = f(state)
Вы описываете вид экрана для определенного состояния, а фреймворк определяет минимальный набор обновлений DOM, необходимых для достижения этого состояния.
Вместо того чтобы писать element.appendChild(...), вы указываете то, что должно быть истинным.
Вот что дал такой подход:
Повторное использование. Создайте компонент один раз, а затем используйте его в десятках мест.
Локальная логика. Вы можете понять один компонент, не читая всё приложение.
Возможность тестирования. Подайте ему параметры, проверьте результат — браузер не требуется.
Составляемость. Маленькие компоненты объединяются, образуя более крупные.
Основной вопрос сместился от "Как создать эту страницу?" к "Из каких компонентов состоит это приложение?"
4. Next.js: от интерфейса к архитектуре
Чисто клиентская версия React имела свои недостатки.
Сначала браузер загружает пустую HTML-структуру. Затем он скачивает крупный пакет JavaScript. После этого выполняется данный пакет, а затем загружаются реальные данные. Только после всего этого пользователь видит какой-либо контент.
Это нормально на мощном ноутбуке. На смартфоне среднего класса с слабым сигналом это означает несколько секунд простоя перед пустым экраном. Кроме того, поисковые роботы могут вообще не индексировать ничего полезного.
Фреймворки вроде Next.js дают разработчикам возможность контролировать где и когда на самом деле генерируется HTML.
SSG. Рендеринг происходит во время сборки, а статический файл доставляется через CDN. Это самый быстрый вариант, хотя контент остается неизменным до следующей сборки.
ISR. Контент остается статическим, но перегенерируется на фоне через определённое время. Вы получаете скорость статического контента при условии его относительной свежести.
SSR. Рендеринг происходит на сервере для каждого запроса. Контент всегда актуален, но сервер выполняет новую обработку каждый раз.
CSR. Рендеринг происходит полностью в браузере. Этот вариант подходит для экранов, доступ к которым ограничен логином, где индексация не имеет значения.
Ключевой момент: выбор осуществляется для каждого маршрута отдельно, а не для всего приложения сразу.
Маркетинговые страницы выигрывают от использования SSG. Страницы продуктов — от ISR. Панели управления — от CSR.
Именно здесь на сцену выходит гидратация. Сервер отправляет HTML, а затем клиент перезапускает дерево компонентов для подключения обработчиков событий. Визуально страница кажется готовой ещё до того, как становится действительно интерактивной.
React Server Components и стриминг существуют в основном для устранения этого разрыва. HTML отправляется сразу, как только он становится доступным, а для тех частей страницы, которые не предназначены для интерактивности, отправляется меньше JavaScript.
На этом этапе работа над фронтендом перестает быть просто «слоем UI». Решения относительно стратегии отрисовки, маршрутизации и кэширования становятся архитектурными решениями, и именно инженеры фронтенда принимают их.
5. Затем появилась масштабируемость
Создание приложения — это одна из сложностей. Создание приложения, обслуживающего миллионы пользователей, — совершенно другая сложность.
Вопросы, которые вы ставите, начинают меняться:
Что должно происходить, когда API нижнего уровня замедляет работу вместо того, чтобы корректно выдать ошибку?
Как шесть отдельных команд могут выпускать код в одну и ту же базу без конфликтов?
Как распространить изменения только среди 1% пользователей и отозвать их в течение минуты, если что-то пойдет не так?
Как будет вести себя приложение на трехлетнем Android-телефоне в месте, где вы его никогда не тестировали?
Какая из вчерашних развертываний стала причиной сегодняшнего роста количества ошибок?
Это вопросы уровня системы. Просто они возникают в браузере.
6. Скорость — это не дополнительная опция, а базовое требование
Элегантный интерфейс, который загружается шесть секунд, по сути является неисправным. Пользователи уже ощущают задержку, еще не понимая, в чем именно проблема.
Современные принципы оптимизации производительности основаны на измерениях, а не на интуиции. Core Web Vitals предоставляют командам общий язык для обсуждения скорости работы сайта.
LCP (Largest Contentful Paint) отмечает момент, когда основной контент становится видимым. Цель — держать этот показатель ниже 2,5 секунды.
INP (Interaction to Next Paint) показывает, насколько быстро интерфейс реагирует после нажатия или клика. Цель — показатель ниже 200 миллисекунд.
CLS (Cumulative Layout Shift) измеряет степень изменения расположения элементов во время загрузки страницы. Цель — показатель ниже 0,1.
Каждая техника оптимизации соответствует одному из этих показателей.
Разделение кода и его постепенная загрузка. Необходимо отправлять только тот JavaScript, который действительно требуется для текущего маршрута, а не весь пакет приложения. Это улучшает как показатель LCP, так и INP.
Настройка изображений. Используйте современные форматы, правильно задавайте их размеры и всегда указывайте ширину и высоту заранее, чтобы ничего не «прыгало». Это способствует улучшению показателей LCP и CLS.
Разумное использование шрифтов. Загружайте их заранее, применяйте свойство font-display: swap и сокращайте набор символов до тех, которые действительно используются. Это помогает решить проблему CLS.
Распространение через CDN. Предоставление данных с сервера, расположенного географически близко к пользователю, одновременно улучшает почти все показатели.
Избегание нагрузки на основной поток. У браузера есть единственный поток, который отвечает как за отрисовку, так и за выполнение ваших скриптов. Задача, выполняющаяся 300 миллисекунд, замораживает любые действия пользователя на это время. Именно здесь проявляется преимущество технологии INP.
Основной принцип, сформулированный просто:
Обеспечьте лучший опыт использования с минимальным количеством ресурсов.
Одно предупреждение: настраивайте параметры на основе фактически полученных данных, а не предположений. Цифры, собранные с ноутбука разработчика в лабораторных условиях, часто отличаются от того, что испытывают реальные пользователи в реальных условиях, причем именно данные с места использования отражают действительность.
7. Кэширование: избегайте повторного вычисления уже решенных задач
По мере увеличения объема трафика постоянное генерирование одинаковых ответов становится расточительным. Кэширование существует для того, чтобы не выплачивать дважды за одну и ту же работу.
Каждый запрос проходит через несколько уровней, и каждый из них представляет собой возможность прервать выполнение работы.
Browser → CDN → Application / ISR → Redis → Database
Чем ближе к пользователю предоставляется ответ, тем дешевле и быстрее он будет.
Однако чем ближе к пользователю находится данные в кэше, тем сложнее вернуться назад, если они неверны.
Именно это противоречие определяет всю практику использования кэша.
Что должно находиться в кэше? Стабильные данные, доступные всем пользователям и требующие значительных усилий для генерации. Никогда не должны храниться данные, специфичные для конкретного пользователя, если только ключ кэша не учитывает индивидуальные особенности пользователя.
Как долго они должны храниться? Короткий срок действия делает кэш более безопасным, но снижает его эффективность. Длительный срок действия ускоряет работу, но увеличивает риски.
Как его очистить? Варианты включают истечение срока хранения по времени (TTL), автоматическую очистку при наступлении определенных событий (например, при публикации данных) или хеширование имен файлов по их содержимому, что делает ненужной саму процедуру аннулирования данных.
Что происходит, когда поиск не удается? Если срок действия широко используемого ключа истекает, а множество запросов одновременно поступают на сервер, возникает перегрузка. Чтобы избежать этого, используйте механизм блокировки или предоставляйте устаревшую версию, пока отдельный запрос обрабатывает обновление.
Шаблон, который стоит запомнить, — это stale-while-revalidate. Немедленно возвращайте кэшированную версию, тихо обновляйте её на фоне, и следующий посетитель получит актуальный контент. Пользователи ощущают высокую скорость, а вы сохраняете корректность с небольшой задержкой.
Обратите внимание на то, какова реальная цена ошибки здесь. Неисправный кэш не только замедляет работу — он может показать неверную цену, неправильное имя или данные кого-то другого.
Кэширование кажется техникой повышения производительности, но на самом деле является решением вопроса корректности.
8. Структурирование для роста
Когда в одной базе кода начинают работать несколько команд, способ её организации становится настолько же важным, как и скорость её работы.
Границы между модулями. Определите, что будет доступно публично, что останется внутренним, и какие зависимости допускаются.
Контракты API. С типизацией и версионированием, чтобы изменения в бэкенде нарушали только процесс сборки, а не работу сайта в реальном времени.
Где хранится состояние. Состояние сервера, URL и локальное состояние клиента — это совершенно разные категории. Большинство ошибок, связанных со состоянием, возникает из-за путаницы между ними.
Изоляция сбоев. Границы ошибок гарантируют, что один неисправный элемент не повлияет на весь страницу.
Внедрение изменений поэтапно. Флаги функций, тестовые развертывания и возможность мгновенного возврата к предыдущей версии.
Подход меняется с простого запуска функции на обеспечение того, чтобы эта функция не затрудняла все будущие изменения.
9. Общий код в монорепозитории
Когда несколько приложений начинают использовать один и тот же заголовок, одни и те же дизайн-токены и одни и те же инструменты аутентификации, копирование и вставка кода перестает быть устойчивым решением.
Монорепозиторий позволяет командам делиться компонентами интерфейса, системами дизайна, вспомогательными функциями, конфигурацией и определениями типов — единым каноническим источником правды, при этом изменения применяются атомарно ко всем пакетам.
Однако обмен кодом сопряжен с определенными издержками. Каждый общий пакет становится зависимостью, и каждая зависимость по сути представляет собой очередь: обновление одной команды может заблокировать выпуск другой команды.
Фактическая цель — повторное использование без тесной связности.
Полезный хеуристический принцип: делитесь элементами, которые редко меняются и имеют одинаковое значение независимо от места использования — такими как дизайн-токены, компонент кнопки или определения типов.
Избегайте обмена всем тем, что связано с уникальной бизнес-логикой конкретной команды. Дублирование кода часто дешевле, чем использование неподходящей общей абстракции.
Микрофронтенды развивают эту идею дальше, позволяя командам работать независимо за счёт увеличения сложности во время выполнения и дублирования зависимостей. Такой компромисс может быть оправданным для крупной организации с чётким разделением команд. В других случаях он редко окупается.
10. Наблюдение за системой в производстве
В масштабах фразы вроде «у меня всё работает» или «в стейджинге всё было в порядке» теряют свою значимость.
На самом деле вам нужна возможность наблюдать за поведением приложения в производственных условиях, на реальных устройствах и в режиме реального времени.
Отслеживание ошибок в сочетании с загружаемыми картами исходного кода, благодаря чему трейс ошибки указывает на ваш истинный исходный код, а не на минифицированный вариант.
Мониторинг реальных пользователей для сбора данных Core Web Vitals от настоящих пользователей с разбивкой по устройствам, странам и качеству соединения.
Структурированная логгеринг, связанная с идентификатором запроса, который позволяет отследить его путь от браузера до каждого обрабатывающего сервиса.
Уведомления, связанные с конкретными проблемами. Уведомляйте людей, когда показатели ошибок или задержек превышают установленные пороги, а не просто когда растет нагрузка на CPU сервера.
Отслеживание выпусков. Маркируйте каждую ошибку конкретным выпуском, в котором она появилась, чтобы ответ на вопрос «что изменилось?» занимал секунды, а не часы.
Вопросы, на которые позволяет ответить такая система:
Где именно пользователи сталкиваются с ошибками? Какой вызов API ухудшает работу приложения? Какой маршрут работает медленно и для какой группы пользователей? Ухудшил ли самый недавний выпуск ситуацию по сравнению с предыдущими версиями?
Раньше код фронтенда был самой сложной частью стека для отслеживания. Теперь это уже не обязательно так.