Серверный рендеринг в консоли
Text.js 0.4 может рендерить страницу на сервере и передавать её кадры в консоль потоком изменений ячеек. Для статических маршрутов первый кадр проходит пререндеринг в HTML.
В Text.js 0.4 появилось второе место для рендеринга страницы. По умолчанию дерево React страницы выполняется во вкладке браузера. Страница, экспортирующая runtime = "server", выполняется на сервере, а браузер получает её кадры.
import { Suspense, type ReactNode } from "react";import os from "node:os";import { Box, Text } from "textjs";export const runtime = "server";async function Load(): Promise<ReactNode> { const [one, five] = os.loadavg(); return <Text>{`load ${one.toFixed(2)} ${five.toFixed(2)}`}</Text>;}export default function StatusPage(): ReactNode { return ( <Box flexDirection="column"> <Text bold>Status</Text> <Suspense fallback={<Text color="gray">Reading host…</Text>}> <Load /> </Suspense> </Box> );}Load — компонент async function. На сервере он приостанавливается до ближайшего <Suspense> или loading.tsx, пока его промис не разрешится, и может напрямую читать файлы, базы данных и модули node:*. Асинхронные компоненты загружают данные и не могут использовать хуки. Обычные компоненты вокруг них сохраняют хуки, фокус, useInput, <Link> и useRouter(), которые работают одинаково в обеих средах выполнения.
Загрузка в браузере
Отрендеренная на сервере страница не отправляет React, Yoga или ваши маршруты. Браузер загружает проигрыватель кадров размером около 21 КБ с gzip, который содержит CellBuffer и тот же CssConsoleDriver, что использует клиентский рендерер. Он выводит кадры с той же частотой 15 кадров/с, поэтому отрендеренный на сервере кадр создаёт такой же вызов console.log, как и на клиенте.
Протокол
Каждая вкладка открывает собственную сессию на сервере: корень React, историю маршрутизатора в памяти и буфер ячеек. Один эндпоинт передаёт данные в обоих направлениях:
browser tab ──(GET /__textjs/live, server-sent events)──▶ frame deltasbrowser tab ──(POST /__textjs/live, JSON)──────────────▶ keys, paste, wheel, resize, focus, Back/ForwardСервер отправляет только строки, хеш которых изменился с последнего кадра, полученного этим соединением. Каждая строка передаётся как последовательности текста со ссылками на таблицу стилей, в которую можно только дописывать. Широкие графемы, кластеры эмодзи и полупрозрачные цвета передаются без изменений.
Между кадрами проходит не менее 66 мс, и каждый кадр сравнивается с тем, что сокет принял в последний раз. Когда медленное соединение перестаёт передавать накопленные данные, промежуточные состояния пропускаются, а следующий кадр сразу переходит к последнему состоянию.
Если поток обрывается, проигрыватель переподключается с идентификатором сессии в заголовке x-textjs-live и возобновляет ту же сессию с нового ключевого кадра, сохраняя её состояние. Идентификаторы сессий — случайные 192-битные значения, которые хранятся в памяти, а не в URL. Сессия без соединения прекращает рендеринг и размонтируется через 30 секунд.
Оба эндпоинта отклоняют межсайтовые запросы, а каждое событие ввода проверяется по фиксированной схеме. Сессия принимает не более 64 событий на POST и около 240 в секунду.
Асинхронные компоненты и данные
Промис каждого асинхронного компонента кешируется отдельно для каждой сессии по ключу из компонента и его пропсов, поэтому повторный рендеринг использует уже выполняющийся запрос. Кеш очищается при смене маршрута и при вызове router.refresh():
const router = useRouter();useInput((e) => e.input === "r" && router.refresh());Модули только для сервера именуются *.server.ts. Сборка завершается ошибкой, если код, отправляемый в браузер, импортирует такой модуль или встроенный модуль node:*, и указывает, откуда выполнен импорт.
Частичный пререндеринг
Частичный пререндеринг работает без сервера. Теперь text build один раз рендерит каждый статический маршрут и встраивает его первый кадр в HTML маршрута как небольшой классический <script>, поэтому консоль отрисовывается ещё до загрузки любого модуля.
Оболочка — всё, что рендерится синхронно. Граница <Suspense>, которая ещё ожидает результат, показывает свою заглушку, в том числе для асинхронных компонентов, а fetch() никогда не разрешается во время рендеринга оболочки.
При запуске JavaScript клиентский рендерер или проигрыватель кадров передаёт настоящий кадр потоком. Поскольку версия 0.4 ещё и дописывает кадры вместо очистки, рендерер при гидратации запускается с initialClear: false и выводит данные ниже оболочки, а его занавес убирает оболочку из видимой области за ту же отрисовку DevTools. Консоль очищается ровно один раз, самой оболочкой.
✓ Prerendering console shells (5/5) · 1 server-rendered, frame player 56.5 kB (21.3 kB gzip)Оболочка рендерится с настроенным размером viewport, по умолчанию 120 × 36, поскольку сборка не может знать размер консоли посетителя. При включённом viewport.responsive первый живой кадр подстраивается под размер закреплённой консоли. Динамические маршруты не рендерят оболочку, а ppr: false в text.config.ts отключает пререндеринг.
Выбор среды выполнения
Клиентская среда выполнения остаётся вариантом по умолчанию. Нажатия клавиш отрисовываются в той же задаче, а приложение можно развернуть на любом статическом хостинге. Серверная среда выполнения подходит для панелей мониторинга и администрирования, которые читают закрытые данные, и маршрутов, чьи зависимости вы не хотите включать в браузерный бандл. Одно приложение может сочетать обе среды, а переход между ними загружает целевой URL как новый документ.
Полный справочник доступен в разделе Серверный рендеринг.