Чем больше сообщество вложится в мост, тем мощнее инструмент«DomainCraft» — рабочее название
DomainCraft

21 августа 2026 г. · 5 мин чтения

Худое ядро: −34 зависимости и −20% бинарника

Мы измерили каждую зависимость ядра DomainCraft и заменили почти все на стандартную библиотеку: 40 модулей стало 6, бинарник похудел на 20%, а скомпилированный вывод не изменился ни на байт.

Мы задались простым вопросом: сколько весит каждая зависимость в бинарнике DomainCraft? Ответ оказался настолько показательным, что мы убрали почти все из них.

Эта статья — не про теорию, а про замеры. Пять коммитов ядра, пять сборок, таблица размеров, секундомер. Всё воспроизводимо, всё честно — включая то, где выигрыша не оказалось.

Почему компилятор должен быть лёгким

DomainCraft — это компилятор домена: один domain.yaml превращается в API, типизированный клиент и админку через настоящий конвейер — лексер, парсер, валидатор, IR, рендерер. Весь смысл компиляции из одной модели в том, что контракт не может разойтись между слоями. Если сам инструмент, который этот контракт обеспечивает, тянет за собой сорок сторонних модулей, обещание слабнет, а не крепнет: каждая транзитивная зависимость — это место, где апгрейд, CVE или ломающее изменение могут тихо сломать то единственное, ради чего инструмент существует.

Лёгкое ядро — не косметическая цель. Это тот же принцип, что делает компилятор достоверным: минимум поверхности между моделью и кодом, чтобы на выходе было ровно то, что задаёт модель. Чем меньше у компилятора собственных движущихся частей, тем больше его поведения можно проверить одной стандартной библиотекой.

Откуда взялась задача

Ядро DomainCraft — это CLI на Go, который компилирует доменную модель в код. На момент старта у него было 5 прямых зависимостей и 40 модулей в go.mod. Вопрос был не «как написать меньше кода», а «какие библиотеки можно заменить стандартной библиотекой, если измерить реальную пользу каждой».

План был такой:

  1. Поднять Go с 1.25 до 1.27 (новый рантайм: Green Tea GC, ускоренный malloc) — правкой одной строки.
  2. Убрать sprig — библиотеку 211 функций шаблона, из которых мосты реально вызывали 22.
  3. Убрать 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. Да, за этим проектом я один, и этот анализ я провёл просто потому, что было интересно. Кто бы ты ни был — найди меня в любой соцсети и напиши: «Я вас любил: любовь ещё, быть может…»