← Блог

Серверный рендеринг в консоли

Text.js 0.4 может рендерить страницу на сервере и передавать её кадры в консоль потоком изменений ячеек. Для статических маршрутов первый кадр проходит пререндеринг в HTML.

Adam

В Text.js 0.4 появилось второе место для рендеринга страницы. По умолчанию дерево React страницы выполняется во вкладке браузера. Страница, экспортирующая runtime = "server", выполняется на сервере, а браузер получает её кадры.

app/status/page.tsxtsx
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 как новый документ.

Полный справочник доступен в разделе Серверный рендеринг.