Аргумент в пользу отказа от сборки фронтенда обычно звучит как «проще». Это неточная формулировка и потому плохой аргумент: собранный интерфейс во многих отношениях проще.
Точная формулировка другая: вы меняете один вид издержек на другой. Экономите на обслуживании инструментов сборки и платите за это в исполнении на стороне браузера и в потолке сложности интерфейса. Стоит ли размен — зависит от того, что вы делаете. Ниже — оба конца размена, честно, и граница, за которой ответ меняется на противоположный.
#Что это за подход
Три составляющие, каждая с узкой ролью:
- Серверный рендеринг. Страницы собирает шаблонизатор на сервере. Браузер получает готовый HTML.
- HTMX. Библиотека, позволяющая любому элементу отправить запрос и подставить полученный HTML в указанное место. Обновление части страницы без единой строки на JavaScript и без промежуточного слоя JSON.
- Alpine. Небольшая библиотека реактивности в атрибутах: показать, скрыть, переключить, привязать значение. Локальное состояние компонента без общего хранилища.
Сборки нет. Библиотеки подключаются файлами. В Persona они лежат на собственном сервере, а не берутся из внешней сети доставки — на момент написания это htmx 2.0.4, Alpine 3.14.7 и markdown-it 14.1.0; проверяйте текущие версии в самом проекте.
Самостоятельное размещение — не идеологический выбор. Внешние сети доставки в отдельных регионах отдают файлы медленно или не отдают вовсе, и приложение оказывается сломанным по причинам, к которым вы не имеете отношения и на которые не можете повлиять.
#Сравнение по шести критериям
| Критерий | Без сборки | Со сборкой |
|---|---|---|
| Стоимость обслуживания инструментов | почти ноль | постоянная: обновления, поломки, зависимости |
| Первая отрисовка | быстрая: готовый HTML | медленнее: сначала скрипт, потом данные |
| Объём переданного | HTML крупнее JSON | JSON мельче, но плюс вес приложения |
| Сложное состояние на клиенте | плохо | хорошо |
| Работа без сети | плохо | хорошо |
| Требования к безопасности страницы | строго хуже | лучше |
Первая строка — та, ради которой всё затевается. Проект без сборки не ломается от смены версии инструмента, не требует обновления дерева зависимостей и через два года собирается ровно так же, как сегодня, — потому что собирать нечего.
Последняя строка — цена, и её обычно не называют.
#Цена, которую платят политикой безопасности
Здесь стоит быть конкретными, потому что этот пункт решает больше, чем кажется.
Стандартная сборка Alpine компилирует выражения из атрибутов через конструктор функции. HTMX делает то же самое для своих значений и фильтров событий. Для браузера это означает, что странице требуется разрешение 'unsafe-eval' — иначе реактивность не работает вовсе.
Дальше хуже. Библиотека, которая генерирует CSS прямо в браузере, вставляет блок стилей на каждой странице; атрибуты style в шаблонах делают то же самое (у нас их 448 в 94 шаблонах). Значит нужен 'unsafe-inline' в правилах для стилей. А исполняемые встроенные блоки скриптов и обработчики в атрибутах требуют 'unsafe-inline' уже для скриптов — и его нельзя убирать постепенно: появление первой одноразовой метки отключает это разрешение для всего остального сразу.
Итог: политика безопасности такого приложения слабее той, что получилась бы у собранного. Она всё ещё закрывает загрузку скриптов с чужих доменов, подмену базового адреса и отправку формы наружу — но от внедрения кода в саму страницу не защищает. Разбор с полной политикой и порядком внедрения — в статье про Content Security Policy.
Это и есть честная цена подхода. Не «медленнее» и не «менее модно», а конкретное ослабление одного защитного слоя.
#Что из этого следует на практике
Обновление части страницы становится тривиальным. Обработчик возвращает кусок HTML, атрибут указывает, куда его подставить. Ни отдельного формата обмена, ни клиентской модели данных, ни синхронизации между ними.
Логика остаётся в одном месте. Не бывает ситуации «правило проверки продублировано на сервере и на клиенте и разошлось». Их просто негде разойтись.
Отладка идёт в инструментах браузера без исходных карт. То, что вы видите в отладчике, — это то, что написано в шаблоне.
Первая отрисовка быстрая, но не бесплатная. Готовый HTML показывается сразу, зато генерация CSS в браузере съедает часть выигрыша. Если гоняться за временем загрузки, первым делом надо убирать именно её — предсобранным набором стилей. Это единственное место, где сборка возвращается через заднюю дверь, и её стоит впустить: остальные приёмы ускорения разобраны в гайде про холодный старт приложения.
Кэширование статики становится критичным. Файлы библиотек не меняются между выпусками, и отдавать их без заголовков долгого кэширования — потеря на каждой загрузке. Обратная сторона: закэшированный старый файл. Схема версионирования — в статье про PWA и кэш сервис-воркера.
#Граница применимости
Подход перестаёт работать не постепенно, а довольно резко. Признаки, что вы её перешли:
- Состояние живёт на клиенте дольше одной страницы. Незакрытый черновик, несколько открытых панелей с независимой историей, отменяемые действия с несколькими шагами.
- Интерфейс должен работать без сети. Серверный рендеринг без сервера не рендерит ничего.
- Много мелких обновлений в секунду. Рисование, перетаскивание с предпросмотром, совместное редактирование. Каждый обмен с сервером здесь заметен.
- Один экран агрегирует данные из десятка источников и обновляет их независимо. На сервере это превращается в множество мелких обработчиков, каждый со своими правами доступа.
- Логика в атрибутах перестала помещаться. Выражение в атрибуте, которое не читается с первого раза, — сигнал, что этому компоненту нужен нормальный код.
Один-два признака — терпимо, точечно решается небольшим скриптом. Три и больше — вы боретесь с инструментом, и дальше будет дороже.
Промежуточный вариант, который часто оказывается правильным: основное приложение без сборки, отдельные сложные экраны — собранными компонентами. Ничто не мешает подключить собранный виджет на одну страницу и оставить остальные девяносто процентов интерфейса как есть.
#Где интуиция подводит
«Без сборки — значит несовременно». Способ доставки интерфейса не связан с его качеством. Пользователь видит скорость и поведение, а не устройство конвейера.
«HTMX заменяет JavaScript». Не заменяет и не пытается. Он убирает один класс задач — обновление части страницы по действию. Всё остальное — по-прежнему JavaScript, просто его мало.
«Раз сборки нет, зависимостей нет». Зависимости есть, просто они лежат файлами. И обновлять их надо руками, а значит легко забыть — обновление библиотеки с исправлением уязвимости не приедет само.
«Передавать HTML вместо JSON расточительно». По объёму — да, HTML крупнее. По суммарному весу загрузки — обычно нет: вы не грузите приложение, которое этот JSON превращает в HTML. Считайте оба слагаемых, а не одно.
«Это только для маленьких проектов». Масштаб приложения такому подходу безразличен — у нас на нём работает интерфейс с более чем тысячей серверных маршрутов. Ограничение не в размере, а в сложности клиентского состояния; про то, как при этом устроена серверная часть, — в статье про архитектуру веб-приложения на FastAPI.
#Частые вопросы
#Нужен ли Node.js вообще при таком подходе?
Для работы приложения — нет. Он может понадобиться разово, если вы решите предсобирать CSS вместо генерации в браузере. Это одна команда в конвейере выпуска, а не постоянная зависимость среды разработки.
#Можно ли постепенно перейти с собранного приложения?
Да, и это обычный путь: новые экраны делаются серверным рендерингом, старые остаются. Обратное сложнее — в этом направлении миграция идёт легче.
#Как быть с формами и проверкой ввода?
Проверка на сервере, ответ — обновлённым фрагментом формы с сообщениями об ошибках. Это ровно тот случай, где подход выигрывает: правило проверки существует в одном экземпляре и не может разойтись между сервером и клиентом.
#Что с доступностью для программ чтения с экрана?
Обычно лучше, чем у собранного приложения, потому что содержимое приходит готовой разметкой. Но подстановка фрагментов требует внимания: изменение части страницы надо объявлять живыми областями, иначе программа чтения его не заметит.
#Что делать, если приложение стало медленно загружаться?
Разбираться по этапам, а не менять подход. Типовые виновники в таком стеке — генерация CSS в браузере и статика без заголовков долгого кэширования. Оба чинятся без переписывания интерфейса; общий порядок работы с производительностью сервиса — в статье о том, как поднять свой ИИ-сервис.
#Подходит ли это для мобильного интерфейса?
Да, с одной оговоркой: каждое действие — обмен с сервером, и на плохой мобильной связи это ощущается. Помогают показ состояния загрузки и локальные переключения без обращения к серверу там, где сервер не нужен.