Начало работы с Octane, преемником Inferno
Octane, преемник Inferno, компилирует React-подобные компоненты в прямой DOM-код и объясняет хуки, массивы зависимостей, запуск и beta-статус.
Octane — это JavaScript-фреймворк для UI от Доминика Ганнауэя (Dominic Gannaway), который берёт компоненты, написанные с использованием API React (useState, useEffect, memo, context, порталы, Suspense), и заранее компилирует их в прямой DOM-код, так что в браузер не попадает никакого виртуального DOM.
Если вы работаете с React достаточно долго, некоторые его правила начинают казаться частью самой модели: хуки вызываются в одном и том же порядке при каждом рендере, массивы зависимостей поддерживаются вручную, а условный эффект превращается в выделенный дочерний компонент или в проверку-заглушку внутри тела эффекта. Большинство из этих правил существуют лишь для того, чтобы устраивать runtime-реконсилятор, а реконсилятор — это как раз то, от чего Octane избавляется.
В этой статье разбирается, что меняет Octane, какие из этих изменений вы заметите первыми, как всё это запустить и насколько далеко продвинулся проект.
Ключевые выводы
- Octane компилирует компоненты в стиле React в прямые операции с DOM, убирая виртуальный DOM из поставляемого runtime.
- Идентичность хука определяется его местом в исходном коде, а не порядком вызова хуков, поэтому хук может находиться внутри ветки
ifили после раннего return; единственное размещение, которое компилятор отвергает, — обычные циклы JavaScript. - Массив зависимостей можно не указывать вовсе и позволить компилятору считать замыкание за вас. Если вы пишете массив сами, он означает то же, что и в React; передайте
null, чтобы выполнять код при каждом рендере. - Для опубликованных пакетов нужен Node.js 22.22.2 или новее, а команда
npm create octane my-appразворачивает проект. - Octane сам себя называет бета-версией: runtime, компилятор и пути SSR/гидратации работают, но API всё ещё меняются.
Что такое Octane и кто его создал?
Octane позиционирует себя как преемника Inferno: знакомый вам API React, где компилятор берёт на себя три задачи, которые сегодня выполняет runtime React, а именно виртуальный DOM, упорядочивание хуков и массивы зависимостей. Ганнауэй создал Inferno, а среди его других работ — React, Lexical, Ripple и Svelte.
Именно эта родословная делает проект достойным десяти минут внимания, а не просто закладки. Inferno добивался скорости, делая реализацию виртуального DOM настолько быстрой, насколько это разумно возможно, — и именно эту идею разбирал наш предыдущий обзор Inferno.js. Octane сохраняет ориентацию на производительность и переворачивает механизм: вместо более быстрого diff — никакого diff. Формулировка о преемственности взята из материалов самого Octane; со стороны Inferno соответствующего анонса нет.
Идея компилятора: никакого виртуального DOM в runtime
Там, где React на каждом рендере строит дерево описаний элементов и сверяет его с предыдущим деревом, Octane компилирует каждый шаблон в DOM-узел, который во время исполнения клонируется и патчится напрямую. Сайт документации проекта сам собран на Octane, что служит неплохим сигналом: компилятор справляется с нетривиальным приложением.
Практическое следствие в том, что работа, которую React делает во время исполнения (обход дерева, сравнение пропсов, решение о том, что изменилось), теперь решается на этапе сборки. Проект публикует таблицу бенчмарков на своей главной странице, нормализованную относительно Octane по нескольким наборам тестов. Это собственные цифры проекта, измеренные самим проектом, причём страница не указывает ни оборудование, ни дату запуска, так что относитесь к ним как к утверждению, требующему проверки, а не как к независимому результату.
Хуки отслеживаются по месту вызова, а не по порядку вызова
Octane определяет идентичность каждого хука по месту, где он появляется в исходном коде, а не по порядку вызова хуков, и выводит опущенные списки зависимостей для эффектов и мемоизации из замыкания. Именно поэтому хук внутри условия — это нормально. Это изменение с наибольшими последствиями в повседневной работе.
Вот форма, которую вы пишете в React, где хук обязан вызываться безусловно:
function Panel({ isEditing }: Props) {
const [draft, setDraft] = useState('');
useEffect(() => {
if (!isEditing) return;
syncDraft(draft);
}, [isEditing, draft]);
if (!isEditing) return <Readonly />;
return <Editor value={draft} onChange={e => setDraft(e.target.value)} />;
}
Состояние и эффект поднимаются выше той ветки, которой они нужны, а логика ветвления дублируется внутри эффекта. В Octane хук располагается там, где ему и место:
function Panel({ isEditing }: Props) {
if (!isEditing) return <Readonly />;
const [draft, setDraft] = useState('');
useEffect(() => syncDraft(draft));
return <Editor value={draft} onInput={e => setDraft(e.currentTarget.value)} />;
}
Что это убирает на практике: дочерний компонент, выделенный исключительно ради того, чтобы сделать хук условным, паттерн «хук вызывается всегда и условно ничего не делает», а также тернарные операторы, существующие только для того, чтобы количество вызовов оставалось стабильным. Обратите внимание, что события в Octane приходят прямо из DOM, поэтому используйте onInput, когда нужно обновление на каждое нажатие клавиши, тогда как onChange срабатывает, когда браузер фиксирует правку.
Единственное ограничение, которое называет проект, — обычные циклы JavaScript. Хуки привязываются к назначенному компилятором месту вызова, поэтому хук со слотом внутри цикла for не имеет стабильной идентичности, и компилятор его отвергает. Выход — список с ключами в шаблоне или отдельный дочерний компонент на элемент.
Почему массивы зависимостей в Octane необязательны?
Опустите список, и компилятор выведет его из замыкания. Напишите массив сами, и он будет работать в точности так же, как в React; передайте null, когда нужно выполнять работу при каждом рендере. Это касается useEffect, useMemo, useCallback и других хуков, принимающих список.
// React: список поддерживаете вы
useEffect(() => {
socket.subscribe(roomId, onMessage);
}, [socket, roomId, onMessage]);
// Octane: компилятор считывает то, что захватило замыкание
useEffect(() => {
socket.subscribe(roomId, onMessage);
});
Возможность отступить от автоматики важна не меньше самого вывода: явный массив никогда не переписывается, так что везде, где нужен точный контроль, просто пишите его. Прямые вызовы встроенных хуков сохраняют этот вывод в любом модуле, который обрабатывает компилятор, включая кастомные хуки в обычных .ts или .js. Вызовы вашей собственной обёртки — более узкий случай: обёртка должна быть объявлена локально в полностью компилируемом модуле .tsrx или .tsx, и она должна передавать свой колбэк и последний параметр с зависимостями напрямую в поддерживаемый хук.
Как запустить octanejs?
Для опубликованных пакетов нужен Node.js 22.22.2 или новее. Команда octane create принимает --template spa для клиентского приложения или --template fullstack для маршрутизации, потокового SSR, гидратации и production-сборки; если флаг не указать, она спросит сама.
npm create octane my-app
cd my-app
npm run dev
Зависимости установит тот менеджер пакетов, которым вы запустили команду, потому что в новой пустой директории нет lock-файла для чтения, — именно поэтому в документации и в репозитории для одного и того же шага показаны разные менеджеры пакетов. Для уже существующего проекта руководство по быстрому старту описывает путь через Vite: установите octane и @octanejs/vite-plugin, затем добавьте плагин. Этот плагин приносит с собой компилятор. Для Rspack используется @octanejs/rspack-plugin, для Rsbuild — @octanejs/rsbuild-plugin.
Коротко о TSRX
TSRX — это синтаксис, на котором пишутся компоненты Octane; он размещается в файлах .tsrx и добавляет шаблонные директивы (@if, @for, @switch, @try) и блоки <style> с изолированной областью действия рядом с разметкой, к которой они применяются. Это самостоятельный языковой проект, а не фича Octane, и Octane — лишь одна из целей его компиляции наряду с React, Preact, Solid, Vue и Ripple. Он также добавляет @{ ... } — сокращение для тела функции, возвращающего один JSX-элемент или фрагмент, где подготовительный код идёт сверху, а итоговый узел выступает результатом. Переходить на него вы не обязаны: руководство «TSRX vs TSX/JSX» подчёркивает, что оба диалекта используют одни и те же хуки, context, порталы, Suspense, переходы, нативные события, изолированные стили, серверный рендеринг и гидратацию, и советует оставить работающий TSX в покое, а не менять расширения ради самого факта.
Каково реальное положение дел у Octane?
Проект называет Octane бета-версией: runtime, компилятор и пути SSR/гидратации работают, но до 1.0 API ещё могут измениться. Changelog Octane относит текущие релизы к линии 0.3, и рекомендация быстрого старта фиксировать версии во всём серьёзном вытекает именно из этого. По собственным подсчётам проекта, основной набор тестов включает более 3900 отдельных поведенческих проверок, распределённых по проверкам соответствия, дифференциальным, гидратации, runtime, компилятора и SSR. Какую долю от покрытия самого React это составляет, отслеживается по каждому случаю в генерируемом отчёте о паритете, а не выводится из общего числа тестов.
Взаимодействие работает в обе стороны. ReactCompat и OctaneCompat оба поступают из точки входа octane/react: первый позволяет реальным компонентам React работать внутри Octane, второй встраивает скомпилированные компоненты Octane в React-приложение. Руководство по совместимости с React пошагово разбирает настройку обоих компиляторов, рендеринг «острова» Octane внутри дерева React, совместное использование React context через границу и серверный рендеринг с гидратацией.
Смотрите на экосистему без иллюзий. Octane поставляет собственные порты @octanejs/* широко используемых React-библиотек, но степень их полноты различается: некоторые соответствуют поведению upstream, другие помечены как частичные или alpha, и генерируемая таблица docs/bindings-status.md — это то место, где стоит проверять, что покрывает пакет, какую upstream-версию он отслеживает, где расходится с ней и обрабатываются ли SSR и гидратация. Подобранный набор собственных биндингов — это совсем не то же самое, что экосистема пакетов, на которую React-приложение опирается, даже не задумываясь.
Octane — на сегодня самый интересный ответ на вопрос о том, как выглядит программная модель React без runtime-реконсилятора, и оба эргономических выигрыша достаточно реальны, чтобы почувствовать их за один вечер. Прочитайте таблицу статуса биндингов по всему, от чего вы зависите, затем разверните одноразовое SPA и поместите хук внутрь ветки условия.
Часто задаваемые вопросы
Можно ли внедрить Octane внутрь существующего React-приложения без переписывания?
Да. Точка входа octane/react экспортирует OctaneCompat, который даёт скомпилированному поддереву Octane место внутри настоящего дерева React 19, так что вы переносите по одному экрану, виджету или компоненту за раз. Внутри такого «острова» use() или useContext читают окружающие его React-контексты, события остаются нативными, а серверный рендеринг работает через импорт хоста из octane/react/server.
Работает ли Context.Provider в Octane?
Нет. В релизе 0.3.0 из клиентских, серверных и нативных контекстов убрали устаревший алиас Context.Provider, и теперь компилятор отвергает статически распознаваемое обращение к Provider и подсказывает, что писать вместо этого. Используйте сам контекст как компонент-провайдер и передавайте ему проп value либо вызывайте createElement(Theme, { value }, children). Consumer с render-пропом тоже удалён, и страница «Differences from React» в Octane сообщает, что его не добавят: хуки со слотами позволяют вызывать use() или useContext внутри условия, а это и есть задача, для решения которой существовал Consumer.
Есть ли хуки, на которые не действует правило Octane «никаких хуков в циклах»?
Да. use() и useContext не занимают слот хука, поэтому их безопасно использовать внутри обычного цикла JavaScript. Любой хук со слотом вместо этого разделял бы одно место вызова между всеми итерациями, что компилятор сообщает как ошибку. Задокументированные способы обойти это — директива @for с ключами, которая даёт каждому элементу собственное состояние хуков, или перенос хука вниз, в дочерний компонент.
Почему onChange в Octane ведёт себя иначе, чем в React?
Octane использует настоящие делегированные DOM-события без слоя синтетических событий, поэтому onChange — это собственное событие change браузера: оно срабатывает, когда правка зафиксирована, обычно при потере фокуса, а не на каждое нажатие клавиши. Для обновлений на каждое изменение используйте onInput. Управляемые поля ввода по-прежнему следуют правилам React для value и checked, а refs передаются как обычные пропсы, а не через объект-обёртку.
Gain Debugging Superpowers
Unleash the power of session replay to reproduce bugs, track slowdowns and uncover frustrations in your app. Get complete visibility into your frontend with OpenReplay — the most advanced open-source session replay tool for developers.
Star on GitHub12k