На этой странице8
21 августа 2026 г. · 5 мин чтения
Худое ядро: −34 зависимости и −20% бинарника
Мы измерили каждую зависимость ядра DomainCraft и заменили почти все на стандартную библиотеку: 40 модулей стало 6, бинарник похудел на 20%, а скомпилированный вывод не изменился ни на байт.
Мы задались простым вопросом: сколько весит каждая зависимость в бинарнике DomainCraft? Ответ оказался настолько показательным, что мы убрали почти все из них.
Эта статья — не про теорию, а про замеры. Пять коммитов ядра, пять сборок, таблица размеров, секундомер. Всё воспроизводимо, всё честно — включая то, где выигрыша не оказалось.
Почему компилятор должен быть лёгким
DomainCraft — это компилятор домена: один domain.yaml превращается в API, типизированный клиент и админку через настоящий конвейер — лексер, парсер, валидатор, IR, рендерер. Весь смысл компиляции из одной модели в том, что контракт не может разойтись между слоями. Если сам инструмент, который этот контракт обеспечивает, тянет за собой сорок сторонних модулей, обещание слабнет, а не крепнет: каждая транзитивная зависимость — это место, где апгрейд, CVE или ломающее изменение могут тихо сломать то единственное, ради чего инструмент существует.
Лёгкое ядро — не косметическая цель. Это тот же принцип, что делает компилятор достоверным: минимум поверхности между моделью и кодом, чтобы на выходе было ровно то, что задаёт модель. Чем меньше у компилятора собственных движущихся частей, тем больше его поведения можно проверить одной стандартной библиотекой.
Откуда взялась задача
Ядро DomainCraft — это CLI на Go, который компилирует доменную модель в код. На момент старта у него было 5 прямых зависимостей и 40 модулей в go.mod. Вопрос был не «как написать меньше кода», а «какие библиотеки можно заменить стандартной библиотекой, если измерить реальную пользу каждой».
План был такой:
- Поднять Go с 1.25 до 1.27 (новый рантайм: Green Tea GC, ускоренный malloc) — правкой одной строки.
- Убрать
sprig— библиотеку 211 функций шаблона, из которых мосты реально вызывали 22. - Убрать
huh— библиотеку TUI-промптов с деревом из 28 модулей — и написать промпты самим.
Каждый шаг измерялся: размер бинарника, количество модулей, скорость. И каждый проверялся дифом «байт в байт»: после удаления библиотеки скомпилированный вывод должен был остаться ровно таким же, как до — контракт, который обеспечивает инструмент, не должен двинуться, пока мы его облегчаем.
Методика
Коммиты, по которым велись замеры:
| Коммит | Что это | Go | sprig | huh |
|---|---|---|---|---|
7fea4e9 |
точка отсчёта | 1.25 | есть | есть |
d2eb243 |
апгрейд тулчейна | 1.27 | есть | есть |
a384657 |
sprig заменён на внутренние функции | 1.27 | нет | есть |
995bfae |
обновлены все библиотеки (cobra 1.10.2 и др.) | 1.27 | нет | есть |
7b9878f |
huh убран, промпты свои (ANSI TUI) | 1.27 | нет | нет |
Сборка как в релизном workflow: go build -ldflags "-s -w". Размер — из ls. Скорость — минимум из 3–5 прогонов time, чтобы отсечь шум ОС.
Размер бинарника
| Сборка | Размер | Модулей в go.mod |
|---|---|---|
7fea4e9 (Go 1.25 + sprig + huh) |
11.81 МБ | 40 |
d2eb243 (Go 1.27 + sprig + huh) |
12.70 МБ | 40 |
a384657 (без sprig) |
11.83 МБ | 32 |
995bfae (обновлённые библиотеки) |
11.67 МБ | 34 |
7b9878f (без huh) |
10.14 МБ | 6 |
Итого от самой тяжёлой сборки до финальной — −2.56 МБ (−20%).
Вклад каждого шага
Go 1.25 → 1.27: +0.89 МБ. Новый рантайм — Green Tea GC, size-specialized аллокации, профили. Это цена апгрейда, и она одинаковая для любого Go-проекта. Зато в обмен — заметное ускорение (см. ниже).
sprig → 22 собственные функции: −0.87 МБ. Sprig поставлял 211 функций шаблона. Мы прогнали все шаблоны пяти мостов через анализатор и выяснили: реально вызываются 22 (около 10%). Остальные 189 — мёртвый груз: десятичная арифметика, генерация сертификатов, кодировки, semver, uuid, регэкспы — и всё это тянуло 9 транзитивных модулей. Заменили на 22 функции на чистой стандартной библиотеке (strings, strconv, path, reflect, time). Результат — тот же вывод, посимвольно.
Обновление библиотек: −0.16 МБ. Бонус: свежие bubbletea/bubbles/lipgloss оказались эффективнее старых, сборка похудела сама по себе (11.83 → 11.67 МБ), хотя модулей стало больше (32 → 34).
huh → свои промпты: −1.53 МБ. TUI-промпты стоили 1.53 МБ и 28 модулей — bubbletea, bubbles, lipgloss, muesli, catppuccin и ещё пара десятков. Ради чего? Ради пяти диалоговых окон: выбор моста, два подтверждения «да/нет» и два мультивыбора файлов.
Go компилирует пакет целиком — «лёгкой версии» huh не существует, как не существует лёгкого bubbletea. Мы проверили и альтернативы: promptkit стоит на том же стеке charmbracelet, survey легче, но всё ещё чужая зависимость.
Поэтому промпты написаны сами: три примитива на bufio + golang.org/x/term — который и так был нужен для определения терминала. Вместо 28 модулей — один файл на полтысячи строк с unit-тестами и ноль новых зависимостей. Внешний вид повторяет huh: тёмные боксы, активная строка и кнопки [Yes]/[No] заливаются зелёным при переключении, стрелки и Space работают в raw-режиме.
Зависимости: 40 → 6
Полный маршрут:
40 модулей (Go 1.25, sprig, huh)
→ 32: убран sprig + mergo, goutils, semver, google/uuid,
shopspring/decimal, spf13/cast, copystructure, reflectwalk, x/crypto
→ 34: обновлены оставшиеся библиотеки
→ 6: убран huh + 27 модулей charmbracelet-экосистемы
Прямых зависимостей осталось три: cobra (CLI-каркас), x/term (определение терминала) и yaml.v3 (парсер YAML).
Отдельная история — xstrings (case-конвертеры). Сначала мы оставили его как «лист без транзитивных зависимостей», но потом портировали его алгоритм snake_case/kebab-case в собственный pkg/textutil (~180 строк, с сохранением лицензии MIT в LICENSE) и убрали и его.
Скорость
Замеры (минимум из прогонов, Windows):
| Сборка | Старт (--version) |
Генерация kitchen-sink (C#) |
|---|---|---|
7fea4e9 (Go 1.25) |
0.081 с | 0.605 с |
d2eb243 (Go 1.27) |
0.080 с | 0.530 с |
995bfae (без sprig) |
0.081 с | 0.524 с |
7b9878f (без huh) |
0.074 с | 0.521 с |
Выводы:
- Старт — шум. Все сборки запускаются за ~0.08 с; разница меньше погрешности измерения.
- Генерация — выигрыш дал именно Go 1.27: −12% (0.605 → 0.530 с). Это Green Tea GC и ускоренные аллокации, которые особенно заметны на аллокационно-нагруженных задачах вроде парсинга и сборки IR.
- Удаление sprig/huh на скорость почти не влияет — они не в горячем пути генерации. Их ценность — не скорость, а размер и независимость.
Что это значит
- Скачивание. Релизный бинарник меньше на ~2.6 МБ. Для CLI-инструмента, который устанавливают через
go installили из GitHub Releases, это реальная разница, особенно на медленных каналах. - Аудит. Меньше кода — меньше поверхность для уязвимостей и меньше чужого в инструменте. Ядро теперь почти полностью стандартная библиотека. Для компилятора, чья задача — быть единым источником правды для целого стека, это важнее, чем для обычного CLI: доверие к выходу не может быть выше, чем доверие к инструменту, который его производит.
- Сопровождение. Каждая зависимость в
go.modтеперь обоснована: либо у неё нет stdlib-замены (YAML, TUI, CLI), либо она уже измерена и признана необходимой. В дереве ничего не прячется случайно. - Принцип компилятора работает. Всё это работает только потому, что DomainCraft — компилятор, а не шаблонизатор с конфигом. У компилятора есть определённый конвейер и фиксированный контракт — убери зависимость, прогон тот же
domain.yaml, и вывод должен быть байт-идентичным. Этот инвариант и позволил резать уверенно: когда контракт — это спецификация, можно уменьшить инструмент и видеть, как вывод стоит на месте.
Вся эта работа вышла в релизе v0.6.0.
P.S. Да, за этим проектом я один, и этот анализ я провёл просто потому, что было интересно. Кто бы ты ни был — найди меня в любой соцсети и напиши: «Я вас любил: любовь ещё, быть может…»