С ИИ фронтенд-фреймворки почти не нужны
- title
- С ИИ фронтенд-фреймворки почти не нужны
- type
- summary
- summary
- Неявность во фреймворках мешает генерации кода ИИ: минималистичный явный паттерн им на замену
- tags
- llm, coding-agent, frontend, typescript
- sources
- vamp-ai-frontend
- created
- 2026-04-11
- updated
- 2026-04-11
- lang
- ru
- translation_of
- vamp-ai-frontend
- source_updated
- 2026-04-11
- translated
- 2026-09-01
- translator
- lllm/antigravity/gemini-3.7-flash-medium
В посте dlants от апреля 2026 года утверждается: когда большую часть фронтенд-кода пишет LLM, сложность фреймворка превращается в обузу. Абстракции React (VDOM, hook'и, reconciliation, concurrent rendering) избавляют человека от дублирования ценой неявного поведения, о котором LLM не может надёжно рассуждать. Если код генерирует агент, повторение ничего не стоит - вместо этого требуются явность, локальность и предсказуемость. В итоге получился vamp - минималистичный паттерн на TypeScript, который "едва ли можно назвать фронтенд-фреймворком".
Паттерн
Каждое view реализует один и тот же интерфейс:
interface View<State, Message> {
container: HTMLElement;
sync(state: State): void;
destroy(): void;
}
Состояние передаётся в одном направлении: событие DOM -> dispatch -> update() мутирует состояние -> view.sync() отправляет изменения в DOM. Здесь нет виртуального DOM, diffing'а и reconciliation. Метод sync() проходит по явным привязкам (bindText, bindAttr, bindClass, bindVisible) и напрямую выставляет свойства DOM. Каждая привязка знает, каким элементом она управляет и за каким полем состояния следит.
Композиция работает через bindChild (статическое монтирование дочернего элемента), bindSlot (условное монтирование и размонтирование дочерних view на основе состояния) и bindList (reconciliation массивов по ключам). Родительские view оборачивают сообщения дочерних элементов в discriminated unions для типобезопасного взаимодействия. Каждая ссылка на DOM использует сгенерированный уникальный ID из ref(), что исключает конфликты селекторов между компонентами.
CSS обрабатывается через cls() и mountStyle() - генерируется уникальное имя класса, внедряется строка CSS, без каких-либо инструментов сборки. Весь фреймворк умещается в один файл TypeScript (vamp.ts), который просто копируется в проект.
Почему этот паттерн подходит LLM
Главная мысль заключается в разнице между кодом, удобным для LLM, и кодом, удобным для человека. Фреймворки для людей минимизируют дублирование, потому что набирать текст долго, а человеку быстро становится скучно. Паттерны для ИИ минимизируют неявное поведение, поскольку LLM не могут рассуждать о правилах, которых не видят в контексте.
Неявность React подробно описана в приложении к посту:
- Reconciliation происходит невидимо. Изменение ветки в условии может незаметно перемонтировать компонент, сбросив фокус ввода и локальное состояние. Разработчик не просил о перемонтировании - алгоритм diffing'а в React решил, что оно необходимо. У LLM, генерирующей код внутри условия, нет никакой возможности узнать об этом, не понимая алгоритм reconciliation в React, которого в самом коде нет.
- Hook'и зависят от порядка вызовов. Каждый
useStateиuseEffectобязан выполняться в одном и том же порядке при каждом рендере. Стоит поместить hook внутрьif, и всё сломается. Массивы зависимостей - ещё один неявный контракт: пропустите зависимость, и получите устаревшие замыкания (stale closures). LLM часто ошибаются в этом месте, потому что правила завязаны на позицию и не видны в коде. - Concurrent rendering приводит к разрывам состояния (tearing - чтение разных версий состояния в рамках одного рендера), неожиданностям со временем выполнения (эффекты срабатывают не тогда, когда вы ожидаете) и проблемам с измерениями (DOM ещё не готов, когда
useLayoutEffectсрабатывает в concurrent mode). Ничего из этого не видно в коде. - VDOM diffing требует ручной оптимизации (
React.memo,shouldComponentUpdate, Immer для иммутабельности), чтобы избежать лишних повторных рендеров. Поведение по умолчанию - "отрендерить всё заново и сравнить diff", что корректно, но медленно.
В vamp ничего этого нет. Метод sync() каждого view представляет собой плоский список явных мутаций DOM. Здесь нет неявного повторного рендеринга, порядка hook'ов, массива зависимостей и diffing'а. LLM читает одно view, видит паттерн и создаёт следующее view, повторяя ту же структуру с другими привязками. Повторение, которое наскучило бы разработчику-человеку, для LLM ничего не стоит: сгенерировать 50 строк вызовов bindText не сложнее, чем 5.
Компромиссы
В посте никто не утверждает, что vamp лучше React для команд разработчиков-людей. Абстракции React существуют потому, что они решают реальные задачи на масштабе: переиспользование компонентов, оптимизация производительности, совместимость с экосистемой. Vamp меняет всё это на читаемость для LLM. Вы теряете экосистему (нет библиотек компонентов, нет React DevTools, нет Next.js), теряете выразительность абстракций (нет собственных hook'ов, нет context provider'ов), но взамен получаете паттерн, который LLM с лёгкостью генерирует без ошибок с первой попытки.
Это частный пример более широкого тезиса из clean-code-coding-agents: структуру кода нужно оптимизировать под того, кто его читает, а если агенты читают его чаще людей, то оптимизировать стоит под агентов. Это также конструктивный ответ на cult-of-vibe-coding: Cohen призывает читать код самостоятельно, а vamp предлагает писать код так, чтобы он был понятен и человеку, и агенту, делая любое поведение явным.
Связь с no-silver-bullet-llms тоньше. Bennett утверждает, что узким местом остаётся проектирование, а не генерация кода. Vamp не спорит с этим: иерархию view, структуру состояния и типы сообщений вы по-прежнему проектируете сами. Но vamp делает этап генерации кода максимально надёжным, убирая специфичные для фреймворка грабли, из-за которых агенты выдают трудноуловимые ошибки. Он борется с узкой привнесённой сложностью (неявное поведение фреймворка, сбивающее LLM с толку), а не с сущностной сложностью проектирования фронтенда.