# Приложение пишем на Vue, а Node входит в гейт и в образ - **Дата:** 2026-08-11 - **Источник:** [../research/spa-framework.md](../research/spa-framework.md) — записка разведки `spa-framework-choice` ## Решение Приложение пишем на **Vue 3** с роутером пятой версии и собираем **Vite** в статику, которую бинарник вшивает через `go:embed` и раздаёт сам. Маршруты задаём своей таблицей через `createRouter`; сборочную надстройку роутера под маршруты по файлам не включаем. Вместе с этим в проект входит **шаг сборки статики**: Node и `npm` становятся нужны на машине разработчика, отдельным шагом в `task gate` и слоем сборки в `Dockerfile`. Это два решения, а не одно, но принимаются они вместе: шаг сборки появляется при любом из трёх кандидатов, и отдельно от выбора фреймворка его обсуждать не о чем. ## Почему Vue Разведка мерила три кандидата на одном и том же экране и нашла единственное различие, которое расходится в разы: > Различает единственное — **размер того, что скачивает телефон**, и он > расходится вчетверо. Вшивание в бинарник, цена шага сборки в гейте и установка на телефон у всех трёх оказались одинаковыми и потому ничего не решают. По размеру Vue стоит посередине — 33 326 Б на четыре экрана против 17 314 у Svelte и 72 402 у React. Выбран он не по этому числу, а по устойчивости экосистемы, и оба отвергнутых кандидата отвергнуты с названной ценой: > **Svelte** — легче Vue вдвое, но своего роутера не имеет, а тот, что есть, > держит один человек. Владелец выбрал экосистему, которая переживёт проект, а не > минимальный размер: 33 КБ на телефоне не отличаются от 17 КБ на глаз, а > брошенная зависимость отличается. > > **React** — вчетверо тяжелее Svelte и вдвое тяжелее Vue, а взамен даёт > экосистему, которой приложению на четыре экрана не на что потратиться. Роутер берём пятой версии, а не четвёртой, по тому же доводу: она стабильна с 29 января 2026 и несёт метку `latest`, то есть чинить будут её, а не предшественницу. Её сборочная надстройка добавляет 34 пакета в установку, и это принятая цена; на собранный файл она не влияет и необязательна. ## Почему это ADR Запись проходит триггер **дорогим откатом**: переход на другой фреймворк переписывает все экраны разом, а не один файл. Шаг сборки сюда же — он меняет требования к машине разработчика и к образу, и снять его потом можно только вместе с приложением. Прежнего решения запись не пересматривает: htmx был снят решением о SPA от 2026-08-10, до заведения этого журнала, и парного статуса «заменено на» ставить нечему. ## Последствия - `+` разметка отделена от кода однофайловым компонентом, а роутер и хранилище состояния идут из тех же рук, что и сам фреймворк: третьей библиотеки под них заводить не нужно. - `+` собранная статика — три файла и значок, поэтому `go:embed` берёт каталог обычной строкой, а бинарник остаётся самодостаточным. - `−` **гейт перестаёт зависеть только от Go.** Красный шаг сборки статики становится таким же поводом остановиться, как красный `go build`, а машина разработчика получает второе требуемое окружение сверх `ffmpeg`. - `−` **в образ добавляется слой Node** ради шага, результат которого — три файла; насколько дольше собирается образ и насколько тяжелеет, не замерялось. - `−` в проект приходит `node_modules` на 92 МБ и 84 пакета, за которыми надо следить отдельно от зависимостей Go: `gitleaks` и `golangci-lint` про них ничего не знают. - `−` приложение весит 33 КБ там, где на Svelte весило бы 17. Разница куплена сознательно и обратно не отыгрывается.