Перевёрнутая архитектура
Как коммит React становится одним вызовом console.log в DevTools.
В DOM-фреймворке компоненты превращаются в DOM-узлы, которые браузер размещает и отрисовывает на странице. В Text.js компоненты превращаются в ячейки, а каждый изменённый кадр ячеек становится одним вызовом console.log в DevTools. Мы называем это инвертированной архитектурой.
Конвейер
React ─▶ reconciler ─▶ Yoga ─▶ CellBuffer ─▶ row-hash gate ─▶ RLE ─▶ console.logКаждый этап нужен потому, что консоль — необычный дисплей. Она моноширинная и ориентирована на строки, оформляется через %c, а DevTools решает, когда её перерисовывать.
Реконсайлер
@textjs/react — реконсайлер React 19 ([email protected]) с двумя хост-элементами, tx-box и tx-text, и текстовыми экземплярами для строковых дочерних элементов. Всё, что вы импортируете, от <Box> до <Markdown>, построено на них. Коммиты планируются через requestAnimationFrame, а нажатие клавиши или вставка вызывает синхронный рендеринг в той же задаче.
Макет
Yoga, скомпилированная в WebAssembly, рассчитывает макет дерева с pointScaleFactor = 1, поэтому каждый блок попадает на целую ячейку. Текст измеряется с помощью Intl.Segmenter и таблиц ширины восточноазиатских символов, поэтому 你好 занимает четыре ячейки, а 🚀 — две. Если правка не может изменить размер текстового узла, повторного обращения к Yoga нет.
CellBuffer
Кадр — это сетка из типизированных массивов, выделенных один раз: символы в Uint32Array, цвета текста и фона в упакованном виде 0xRRGGBBAA, атрибуты в виде битовой маски Uint16Array. Отрисовка записывает данные в эти массивы и отслеживает затронутые строки.
Проверка изменений
У каждой строки есть кешированный 32-битный хеш FNV-1a. Хеш пересчитывается только для строк, в которые записывали после последнего кадра. Если ни одна строка не изменилась, драйвер завершает работу до сборки строки и не вызывает консоль. Индикатор загрузки на скрытой вкладке ничего не стоит, потому что не обновляется.
Кодирование длин серий
Изменённые строки повторно сканируются и разбиваются на серии с одинаковым стилем, а каждая серия становится одним %c span. Серии продолжаются через разрывы строк. CSS-строки интернируются под целочисленными идентификаторами стилей, поэтому на горячем пути одна и та же строка никогда не собирается дважды. Панель 100×32 — это 3200 ячеек и 149 span.
Строка формата ещё и защищает от двух особенностей DevTools. Буквальный % записывается как %%. Символ word joiner вставляется после ://, внутри www. и в path:line, чтобы DevTools не превращал ваш текст в ссылку и не перекрашивал его в свои цвета.
Один вызов
Каждый изменённый кадр — это один вызов:
console.log("%c\u200b%c\n%c▼ OpenCode Lite%c …", "margin-top:100vh;", "", "color:#fafafa;…", …);Кадры дописываются с учётом ограничителя частоты DevTools в 50 мс и сменяют друг друга за одну отрисовку. Эта часть описана в статье Turbolog: устранение мерцания.
Ввод
Консоль не может получать нажатия клавиш, поэтому их получает страница. Скрытый <textarea> собирает нажатия клавиш, вставку и композицию IME, а события wheel прокручивают компоненты ScrollBox. На textjs.org страницы Playground и OpenCode представляют собой одну большую панель фокуса. Откройте консоль, затем щёлкните по странице, чтобы печатать.
Что видит страница
Страница не может прочитать размер консоли, только размер своего окна, поэтому Text.js вычисляет его косвенно: закреплённая панель уменьшает innerWidth или innerHeight, тогда как outer* остаётся неизменным, и каждая гипотеза о закреплении проверяется для уровней масштабирования Chrome. Выбирается наиболее точное соответствие. Константы (56 пкс горизонтального отступа, 104 пкс интерфейса консоли, шаг строк 14 пкс) измерены в реальном интерфейсе DevTools.