Классический Open Source держался на пирамиде участия: от новичков, правящих опечатки, до мейнтейнеров ядра. ИИ и корпоративные стратегии ломают эту модель.
Несмотря на то, что Open Source код по-прежнему остаётся открытым – вы можете им пользоваться, читать его, форкать – писать в него всё чаще можно только «своим». И главный сдвиг здесь не юридический, а производственный, потому что на деле Open Source по лицензии ≠ Open Source как культуру участия.
React PR #36173 как симптом
Начнём с React PR #36173.
В июне 2026 года в React был смёржен огромный Pull Request с миграцией React Compiler с JavaScript/TypeScript на Rust. Речь здесь не о косметическом рефакторинге: 435 коммитов, сотни файлов, перенос сложной части инфраструктуры React на другой язык, сохранение поведения и сравнение промежуточного состояния компилятора pass-by-pass.
В описании PR Joe Savona отдельно пишет, что архитектура, стратегия тестирования и подход к миграции были human-led, т.е. осуществлялись человеком, но большая часть кода была написана с AI assistance.
Сухие цифры из PR:
435 коммитов
461 изменённый файл
+123 289 строк
масштабная миграция React Compiler на Rust
pass-by-pass сохранение поведения TypeScript-версии
human-led архитектура и стратегия тестирования
большая часть кода, по словам автора PR, написана с AI assistance.
В старой Open Source-модели такую задачу можно было бы представить как долгий процесс из множества маленьких PR, обсуждений, ревью и постепенного вовлечения внешних участников. Но получилось по-другому, и публичным стал в основном результат.
Да, код открыт. PR можно прочитать, формально любой может оставить комментарий. Но производственная работа проведена внутри. Архитектурное решение принято. Основная реализация сделана. Внешний мир получает не возможность участвовать в направлении проекта, а возможность посмотреть на уже выбранное.
Работа шла внутри узкого круга людей, связанных с React Compiler: Michael Vitousek, Lauren Tan, Joe Savona и другие участники core-команды. Архитектура оставалась под контролем людей, но большая часть механической реализации была сделана с помощью ИИ.
Существенно другое: внешний контур не стал производственной силой в этой миграции. Сообщество могло смотреть, обсуждать, реагировать, пробовать после появления кода и находить баги. Но сама работа происходила как внутренняя инженерная задача.
Именно поэтому PR #36173 так показателен: формально это Open Source-репозиторий, но разработка критической части проекта происходила как корпоративная разработка. Код открыт для чтения, но его написание осталось внутри контролируемого контура.
Техническая цена: rust-порт как транслитерация
Здесь я переключусь с публичной картинки на собственные наблюдения изнутри форка. Когда я начал работать с rust-портом React Compiler, я ожидал увидеть переработанный IR с нуля. Rust – – другой язык: тут естественны Copy ID’шники, arena-аллокация, interning для строк, более явное разделение владения и мутабельности. Хороший порт не просто переписывает синтаксис, а постепенно перестраивает dataflow под идиомы языка.
Вместо этого rust-порт в значительной части выглядит как транслитерация TypeScript-версии. Структура IR повторяет оригинал очень близко, только переписана в rust-синтаксис. Несколько конкретных мест, которые бьют по глазам.
Свой AST-слой поверх Babel JSON.
Самое странное место – собственный AST-слой поверх Babel JSON.
Вместо того чтобы опереться на уже существующие rust AST из swc_ecma_ast или oxc_ast, в compiler/crates/react_compiler_ast/ появляется отдельный слой структур под Babel AST: File, Program, Statement и так далее. А compiler/crates/react_compiler_lowering/src/build_hir.rs превращается в тысячи строк ручного lowering'а из Babel-like AST в HIR.
Можно, конечно, сказать, что React Compiler должен интегрироваться с Babel, OXC и SWC, поэтому нужен общий AST-представитель. Но логичным образом появляется вопрос, а нужна ли эта интеграция такой ценой в принципе? Если компилятор уже переписывают на Rust, почему центральным форматом остаётся Babel-shaped AST, а не нативная rust-модель? Почему вместо использования существующих решений из rust-экосистемы появляется ещё один AST-слой?
В старой TypeScript-версии это было понятно: Babel AST был естественной средой обитания. В rust-порте это выглядит как наследование старой архитектуры, а не как осознанное проектирование новой.
Owned String вместо OxcAtom.IdentifierName::Named(String) хранит отдельную аллокацию для каждого identifier'а. Два Identifier с именем "count" – это две независимые строки в куче.
Это особенно показательно на фоне oxc_ast, где идентификаторы используют Atom: имя интернируется один раз, а дальше по дереву ходит компактное значение. Сравнение имени превращается не в побайтовое сравнение строк, а в сравнение atom'ов.
В rust-порте React Compiler этот слой отсутствует. Имена остаются owned-строками, как будто мы всё ещё живём в JavaScript-модели объектов и строк, а не пишем compiler IR на Rust.
Один большой mutable environment.Environment.identifiers: Vec<Identifier> и IdentifierId(u32) – это уже ближе к rust-модели: cheap-Copy ID указывает в arena-like storage. Но дальше многие pass'ы всё равно работают через большой &mut Environment, где живут identifier'ы, типы, scope'ы и outlined-функции.
Проблема не в том, что в коде не используется какой-то конкретный rust-трюк вроде split_at_mut. Проблема в том, что общая модель остаётся похожей на TypeScript-версию: один большой mutable object graph, который pass'ы последовательно мутируют. Rust здесь скорее ограничивает форму записи, чем меняет архитектуру.
Всё вместе даёт rust-порт, который выглядит как rust-проект, но структурно во многом остаётся TypeScript-проектом в rust-синтаксисе. Бенчмарки будут лучше TS-версии – нативный код против JavaScript runtime. Но это скорее выигрыш от переноса исполнения в native, чем переосмысление архитектуры под язык, в котором половина идиом из оригинала просто не нужна.
И вот тут начинается часть, которая напрямую связана с основной темой статьи.
Если бы rust-порт развивался как открытая переработка IR, в нём нашлись бы контрибьюторы, которые спросили бы: «зачем нам центральный Babel-like AST-слой?», «почему имена не интернированы?», «почему HIR настолько буквально повторяет TS-структуру?», «можно ли развести environment на более узкие storage'и?». Это ровно тот класс задач, которым жила классическая Open Source-модель: маленькие точечные улучшения от людей, которые хорошо знают свою часть экосистемы.
Но rust-порт изначально развивался как внутренняя задача Meta с AI assistance. Внешний ревьювер видит уже почти готовый производственный блок: 435 коммитов, 461 файл, 123 289 строк. Архитектурные решения уже приняты. Идиоматические альтернативы уже не обсуждаются на равных.
Техническая цена буквального порта будет платиться годами. Каждый новый pass унаследует те же assumptions. Каждый новый контрибьютор будет писать в стиле «Rust как TypeScript», потому что код вокруг именно такой. А переписать IR с нуля посреди активного портирования pass'ов – это уже не оптимизация, а остановка конвейера, на которую Meta вряд ли пойдёт.
Signals как второй симптом
Но проблема не только в том, как Meta пишет код. Проблема ещё и в том, какие архитектурные направления вообще получают право стать частью React.
В React-сообществе регулярно поднимается тема более точечной реактивности: изолированные обновления, меньше Context-ререндеров, меньше ручной мемоизации, меньше dependency arrays и меньше магии вокруг вопроса «почему компонент снова перерендерился». Тут часто вспоминаются Solid.js, Vue, Preact Signals, Angular Signals, Jotai atoms, Redux selectors и др. модели source/sink. Запрос на более granular reactivity в React существует давно, и signals – один из самых заметных языков, на котором этот запрос формулируют.
После появления React Compiler signals уже не выглядят как «невозможная магия». Идея довольно естественно ложится в уже существующую архитектуру компилятора. React Compiler не просто бегает по AST и заменяет одни ноды на другие. Его документация описывает build-time инструмент, который автоматически оптимизирует React-код через memoization и избавляет от части ручных useMemo, useCallback и React.memo.
В описании Rust PR дополнительно видно, что внутри компилятор lowering'ит AST в HIR, использует control-flow graph, SSA и серию pass'ов, которые сохраняют поведение TypeScript-версии pass-by-pass.
Сноска про HIR. Примеры
SignalRead,SignalWrite,c_signal,signalScope– это не существующий API React. Это концептуальная модель: как signals могли бы интегрироваться в pipeline React Compiler. Автор моделирует такую идею в rust-порте React Compiler, чтобы проверить, что она вообще технически представима.
Сегодня React Compiler пытается автоматически заменить ручную мемоизацию. Он анализирует компонент, находит куски кода, которые можно переиспользовать, и генерирует memo cache.
Условно, если у нас есть компонент с локальным состоянием и derived data:
function TodoList({items}) { const [query, setQuery] = useState('') const visibleItems = items.filter(item => { return item.title.includes(query) }) return ( <> <SearchInput value={query} onChange={setQuery} /> <TodoItems items={visibleItems} /> </> ) }
компилятор может построить reactive scope примерно такого вида:
scope#1: deps: - items - query body: visibleItems = items.filter(item => item.title.includes(query)) jsx = <TodoItems items={visibleItems} />
а дальше при codegen это превращается в memo-cache логику:
если items и query не изменились: вернуть старые visibleItems и старый jsx иначе: пересчитать visibleItems пересоздать jsx для TodoItems сохранить результат в cache
т.е., React Compiler уже строит внутреннюю модель:
какой кусок UI
от каких значений зависит
и когда его нужно пересчитать
Signals можно было бы вставить ровно в эту точку.
Как signals могли бы лечь в эту модель
Допустим, у нас есть signal:
function Counter({multiplier}) { const count = useSignal(0) return <div>{count * multiplier}</div> }
Без signal-aware анализа count – это обычное чтение свойства или локального значения. Компилятор не знает, что за ним стоит реактивный источник.
Но если React Compiler знает, что count — это Signal<number>, обращение к count можно представить как специальную HIR-инструкцию:
block#0: %count = HookCall useSignal(0) %v0 = SignalRead %count %v1 = BinaryExpression "*", %v0, multiplier %jsx = JSXElement "div", %v1 return %jsx
Тогда reactive scope становится не таким:
scope#1: deps: - count - multiplier
а таким:
scope#1: normalDeps: - multiplier signalDeps: - count body: %v0 = SignalRead count %v1 = %v0 * multiplier return <div>{%v1}</div>
Это ключевая разница.
multiplier – обычная зависимость из props. Её можно проверять через memo cache.
count – signal-зависимость. Она не просто сравнивается при следующем render. Она может сама инвалидировать scope, который её прочитал.
На уровне runtime это можно представить так:
scope#1 подписан на signal count count.set(next): отметить scope#1 dirty запланировать обновление
То есть useMemo(fn, deps) во многих случаях заменяется не пользовательским hook-API, а compiler-механикой:
pure computation ↓ reactive scope ↓ normal deps + signal deps ↓ memo cache или signal-aware cache
И здесь signal даёт более прямую модель: не «угадай dependency array», а «кто прочитал signal — тот и зависит от signal».
useSyncExternalStore
Самый логичный пример здесь даже не useMemo, а useSyncExternalStore.
Сегодня external store в React часто выглядит так:
function Counter() { const count = useSyncExternalStore( store.subscribe, store.getSnapshot, ) return <div>{count}</div> }
Это hook-level подписка. При изменении store React должен заново вызвать компонент, получить snapshot и пройти reconciliation. Даже если пользовательская библиотека хорошо оптимизирует selector, сама модель всё равно начинается с подписки компонента на внешний источник.
Signal-style API мог бы выглядеть так:
function Counter() { return <div>{store.count}</div> }
Если store.count известен компилятору как signal, HIR мог бы фиксировать не обычный property access, а реактивное чтение:
block#0: %store = LoadGlobal "store" %countSignal = PropertyLoad %store, "count" %count = SignalRead %countSignal %jsx = JSXElement "div", %count return %jsx
И scope получал бы зависимость не от всего компонента, а от конкретной observable cell:
scope#1: signalDeps: - store.count body: return <div>{SignalRead(store.count)}</div>
Т.е. вместо hook-level подписки всего компонента можно получить dependency конкретного reactive scope от конкретного внешнего значения.
Это не значит, что любой существующий useSyncExternalStore можно механически переписать в signal. У external store может быть произвольная семантика snapshot’ов, сравнения, подписок и teardown. Но для официального signal API у компилятора появился бы явный контракт: store.count – не произвольный property access, а реактивное чтение. С локальным состоянием компонента граница проходит в другом месте.
useState
Из примера с external store не следует, что useState тоже можно автоматически заменить на signal.
useState привязан к Fiber, scheduler, batching, transitions, Strict Mode, hydration, DevTools и правилам хуков. React Compiler сам осторожно относится к hook calls: scopes с hook calls нельзя произвольно переносить или делать conditional.
Поэтому правильная модель не такая:
useState → signal
А такая:
useSignal() остаётся hook'ом, но чтение signal внутри render становится SignalRead в HIR.
Если говорить совсем конкретно, signals могли бы потребовать от compiler примерно таких изменений:
type inference распознаёт официальный signal API
HIR получает
SignalRead(signal)иSignalWrite(signal, value)dependency propagation добавляет
SignalDependencyreactive scopes не сливаются слишком грубо, иначе granular reactivity теряет смысл
codegen/runtime получает signal-aware cache или primitive вроде
signalScope(...).
Это всё ещё не «добавить один if»; остаются scheduler, Concurrent React, Suspense, hydration, Server Components, teardown подписок, DevTools и совместимость со старым кодом. Но техническое место для signals в React Compiler вполне понятно.
Почему это связано с OSS
Вот теперь возвращаемся к Open Source.
Запрос на granular reactivity в React не выглядит фантастическим. После React Compiler у Meta уже есть инфраструктура, в которую такая модель могла бы лечь: HIR, reactive scopes, dependency propagation, memo cache, runtime. Но Meta выбрала принципиально другой путь.
Пока Vue, Solid, Preact, Angular и Svelte в разных формах приходят к fine-grained reactivity, React пытается решить ту же боль не новой реактивной моделью, а компилятором поверх старой модели render → diff → memoization.
Именно здесь технический спор превращается в организационный. Проблема не в том, что signals невозможно вписать в React Compiler, а в том, что владелец проекта не хочет менять идеологию React в эту сторону. Сообщество может годами обсуждать signals, сравнивать React с Solid, Vue, Preact и Angular, приносить реальные production-кейсы и предлагать API. Но если внутри Meta принято решение, что React должен идти через Compiler + automatic memoization, а не через signals, то на этом разговор фактически заканчивается. Не потому что signals невозможно вставить в compiler pipeline, а потому что направление развития контролируется внутренним контуром Meta.
Открытый репозиторий не означает открытое управление. И уж точно не означает, что архитектурные ожидания сообщества имеют такой же вес, как идеологическое решение компании, которая контролирует проект.
Сообщество – больше не часть процесса
Раньше сообщество было частью производственной модели Open Source: внешние контрибьюторы закрывали баги, писали тесты, правили документацию, предлагали патчи и постепенно врастали в проект. Теперь в Open Source-проектах эта, казалось бы, естественная парадигма всё дальше от реальности.
Сообщество всё реже нужно как производственная сила. Его надо растить, вводить в контекст, проводить через ревью и доводить до уровня мейнтейнеров, а это хлопотно + теперь часть этой работы можно заменить маленькой внутренней командой, усиленной ИИ.
Что остаётся внешним разработчикам?
Писать issue.
Ловить баги.
Создавать шум вокруг проблем.
Генерировать идеи, которые потом всё равно реализуют внутри.
То есть сообщество превращается не в партнёра по разработке, а во внешний сенсор.
Сообщество полезно, пока поставляет сигналы: баги, жалобы, edge cases, реальные сценарии использования, но к самому написанию кода его всё меньше подпускают. И к этому надо быть готовым, когда пытаетесь внести вклад в Open Source.
Снаружи всё часто выглядит как открытая разработка. На практике же можно столкнуться с закрытым контуром решений, усталостью от внешних PR, нежеланием объяснять контекст, игнорированием предложений и ощущением, что сообщество нужно не как соавтор, а как бесплатный тестировщик. Это не значит, что в OSS не надо идти, но туда точно не стоит идти с наивной уверенностью, что раз репозиторий открыт, значит, вас там ждут.
Open Reading, Closed Writing
Формально Open Source никуда не исчезает: код открыт, его можно читать, форкать, использовать и изучать. Но открытость кода и открытость процесса – это разные вещи. Классический Open Source был ценен тем, что внешний человек мог не только читать код, но и участвовать в его написании: пройти через ревью, понять архитектуру, вырасти внутри проекта и со временем стать частью ядра. Классический Open Source был ценен тем, что внешний человек мог не только читать код, но и участвовать в его написании: пройти через ревью, понять архитектуру, вырасти внутри проекта и со временем стать частью ядра.
Сейчас крупный корпоративный Open Source всё больше движется к другой модели:
Open Reading, Closed Writing.
Читать можно всем.
Писать в основном своим.
Проекты остаются открытыми как артефакты, но становятся всё более закрытыми как производственные процессы.
Вместо вывода
Главный вывод здесь не в том, что Open Source умер юридически.
код открыт
репозитории живы
лицензии на месте
проекты развиваются.
Умирает другое: старая культура участия, в которой внешний разработчик мог постепенно пройти путь от маленьких PR до реального влияния на проект.
Если вы хотите идти в Open Source с наивной идеей «сейчас я начну контрибьютить, меня заметят, примут, проведут через ревью и постепенно пустят в ядро проекта» лучше остановиться и сто раз подумать.
А вас там вообще ждут?
Не как пользователя.
Не как человека, который поставит звёздочку.
Не как того, кто напишет issue или принесёт баг-репорт.
А именно как участника разработки.
React PR #36173 хорошо показывает новую реальность: огромная миграция компилятора на Rust – и всё это не результат массового движения сообщества, а работа маленькой внутренней команды, усиленной ИИ. История с signals показывает вторую сторону той же проблемы: внешняя аудитория может обсуждать, спорить и предлагать API, но, если владельцы проекта выбрали другой путь, внешний голос редко становится решающим.
ИТОГО: прежде чем лезть в OSS-проект, стоит честно спросить себя:
этот проект реально принимает внешние PR?
есть ли у внешних контрибьюторов шанс влиять на архитектуру?
или от сообщества ждут только баги, issue и бесплатное тестирование?
Open Source по лицензии ещё не означает Open Source как культуру участия.
И если вы идёте туда за опытом, ростом и признанием, сначала убедитесь, что дверь действительно открыта, а не просто стеклянная.