Серверный рендеринг и частичный пререндеринг

runtime = "server": маршруты рендерятся на сервере и передаются потоком кадров; пререндеринг оболочек консоли.

Рендерите React-дерево маршрута на сервере и передавайте его кадры потоком в консоль. Выполняйте пререндеринг первого кадра каждого статического маршрута в его HTML.

Две среды выполнения

Каждая страница рендерится в одном из двух мест:

Клиент (по умолчанию)Сервер (runtime = "server")
Где выполняется React-деревоВкладка браузераtext dev / text start / адаптер bun (Bun или Node)
Что скачивает браузерВсё приложение: React, Yoga WASM, ваши маршрутыПроигрыватель кадров, около 21 КБ gzip
Доступ к даннымfetch() к обработчикам маршрутовНапрямую: базы данных, файлы, node:*, секреты
Асинхронные компонентыИспользуйте use() или эффектыПоддерживаются компоненты async function
Задержка вводаВ той же задаче, синхронноОдин цикл обмена с сервером
Работа на статическом хостингеДаНет, нужен text start или bun dist/server.js

Обе среды выполнения создают одинаковые кадры. Проигрыватель кадров кодирует ячейки тем же CssConsoleDriver и выводит их с той же частотой ~15 кадров/с, поэтому отрендеренный на сервере кадр побайтово совпадает с вызовом console.log на клиенте.

Включение

Экспортируйте runtime из страницы:

app/status/page.tsxtsx
import { Suspense, type ReactNode } from "react";import os from "node:os";import { Box, Text } from "@textjs/app";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>  );}

runtime считывается для каждой страницы как статический строковый экспорт (аннотация типа, as const или реэкспорт допустимы; вычисляемое значение нет). Макеты, loading.tsx, error.tsx и not-found.tsx рендерятся там же, где их страница: общий макет клиентских и серверных страниц выполняется в обоих местах и отправляется в браузер, а макеты и границы сегмента, содержащего только серверные страницы, остаются на сервере. Хуки, фокус, useInput, <Link> и useRouter() работают так же, как на клиенте.

Выбор среды выполнения

Используйте клиентскую среду выполнения (по умолчанию) для:

  • Интерфейсов с интенсивным вводом, где каждое нажатие клавиши должно отображаться в той же задаче, например редакторов и фильтров.
  • Приложений, развёртываемых на статическом хостинге или CDN.
  • Инструментов с поддержкой работы офлайн.

Используйте серверную среду выполнения для:

  • Панелей мониторинга и администрирования, которые читают базы данных, файлы, очереди или закрытые API. Учётные данные остаются на сервере.
  • Маршрутов с тяжёлыми зависимостями (SDK, парсерами, таблицами синтаксиса), которые вы не хотите включать в браузерный бандл.
  • Страниц, которые должны быстро загружаться в медленных сетях. Браузер скачивает проигрыватель размером 21 КБ (gzip) вместо всего приложения.

Вы можете сочетать среды выполнения в одном приложении. При переходе между ними целевой URL загружается как новый документ. В браузерном бандле каждая серверная страница заменяется заглушкой, поэтому серверные модули и всё, что они импортируют, никогда не отправляются на клиент.

Называйте модули, предназначенные только для сервера, *.server.ts. Сборка завершается с ошибкой и указывает импортирующий модуль, если код, отправляемый в браузер (клиентская страница или используемый ею макет), импортирует модуль *.server.* или встроенный модуль node:*.

Middleware защищает серверные маршруты так же, как документы: выполняется для целевого маршрута перед открытием сессии, перед каждым переходом внутри сессии (router.push, «Назад» и «Вперёд»), а затем при каждом изменении cookies сессии или Authorization. Если middleware должен перенаправить, отклонить или переписать запрос, браузер вместо этого загружает URL как документ, а middleware отвечает на этот запрос.

Асинхронные серверные компоненты

На сервере любой компонент async function приостанавливает рендеринг до ближайшего <Suspense> или loading.tsx, пока его промис не разрешится:

app/deployments/live/page.tsxtsx
export const runtime = "server";async function Deployments({ team }: { team: string }) {  const rows = await db.deployments.list({ team });  return <Table data={rows} columns={COLUMNS} />;}

Каждый промис кешируется в пределах сессии с ключом из компонента и пропсов, поэтому повторный рендеринг использует уже выполняющийся запрос. Кеш очищается при смене маршрута и при вызове router.refresh(). Чтобы повторно запросить данные после ввода, вызовите router.refresh():

const router = useRouter();useInput((e) => e.input === "r" && router.refresh());

Пропсы, передаваемые асинхронным компонентам, должны быть сериализуемыми: строки, числа, простые объекты, массивы, Date, URL, Map, Set и элементы. Пропсы-функции не входят в ключ (передавайте данные, а не колбэки). Каждая сессия кеширует не более 256 промисов. Вы можете вручную обернуть компонент в serverComponent(fn) из @textjs/app/server-components.

Хуки (useState, useInput, …) не работают внутри компонента async function. Для взаимодействия рендерите обычный компонент вокруг него или ниже.

Принцип работы

browser tab ──(GET /__textjs/live, server-sent events)──▶ frame deltasbrowser tab ──(POST /__textjs/live, JSON)──────────────▶ keys, paste, wheel, resize, focus, Back/Forward
  • Одна сессия на вкладку. Каждая загрузка страницы открывает собственную сессию: корень React, историю маршрутизатора в памяти и буфер ячеек на сервере. Идентификаторы сессий — случайные 192-битные значения, которые браузер хранит только в памяти.
  • Изменения ячеек. Сервер отправляет только строки, хеш которых изменился с последнего кадра, полученного этим соединением. Каждая строка передаётся как последовательности текста со ссылками на таблицу стилей, в которую можно только дописывать, при этом широкие графемы, кластеры эмодзи и полупрозрачные цвета сохраняются точно.
  • Обратное давление. Между кадрами проходит не менее 66 мс, и каждый сравнивается с последним состоянием, принятым сокетом. Когда медленное соединение перестаёт передавать накопленные данные, промежуточные состояния пропускаются, и следующий кадр сразу переходит к последнему состоянию.
  • Повторные подключения. Если поток обрывается, проигрыватель подключается повторно с идентификатором сессии в заголовке x-textjs-live (никогда в URL) и продолжает ту же сессию с сохранённым состоянием, начиная с нового ключевого кадра. Пропущенные обновления адресной строки воспроизводятся повторно. Сессия без соединения прекращает рендеринг и размонтируется через 30 секунд; если сервер её забыл, проигрыватель открывает новую.
  • История. «Назад» и «Вперёд» перемещают обе истории на одинаковое число записей, а router.back() на сервере перемещает и историю браузера.
  • Безопасность. Обе конечные точки отклоняют межсайтовые запросы (Sec-Fetch-Site и Origin; X-Forwarded-Host учитывается только с server.trustProxy). POST-запросам ввода нужны тело JSON и заголовок x-textjs-live. Каждое событие проверяется по фиксированной схеме, а цели переходов должны быть путями на вашем origin. Рендерить можно только страницы из вашего манифеста app/. Сессии привязаны к cookies, с которыми они были открыты.
  • Ограничения. Не более 64 событий на POST и около 240 в секунду на сессию (при превышении — 429), тайм-аут тела запроса 5 секунд, 32 сессии на адрес клиента и maxSessions на процесс (первой удаляется сессия, отключённая дольше всех).
text.config.tsts
export default defineConfig({  live: { maxSessions: 512, reconnectGraceMs: 60_000 },});

Частичный пререндеринг

text build рендерит каждый статический маршрут один раз с заданным размером viewport и встраивает первый кадр в HTML маршрута как небольшой классический <script>. Консоль отрисовывается до загрузки любого модуля:

  1. Статическая оболочка. Всё, что рендерится синхронно. Каждая граница <Suspense>, которая ещё ожидает, показывает запасное содержимое, включая асинхронные серверные компоненты (они не выполняются во время рендеринга оболочки), use() и лениво загружаемые модули. fetch() никогда не разрешается во время рендеринга оболочки, поэтому данные, загружаемые в эффектах, тоже остаются в состоянии загрузки.
  2. Динамические области. Когда запускается JavaScript, клиентский рендерер или проигрыватель кадров передаёт настоящий кадр потоком. Он дописывается под оболочкой, а собственная шторка гидратирующего кадра убирает оболочку из вида за ту же отрисовку DevTools. Оболочка записывает время своей отрисовки, поэтому первый гидратирующий кадр пережидает окно DevTools после очистки, вместо того чтобы попасть на медленный путь отрисовки.

Во время загрузки только оболочка очищает консоль. Гидратирующий рендерер запускается в режиме дописывания (initialClear: false), поэтому ничего не мигает, а оболочка отрисовывается только на том пути, для которого была отрендерена. Динамические маршруты ([id], force-dynamic) не рендерят оболочку.

 ✓ Prerendering console shells (5/5) · 1 server-rendered, frame player 56.5 kB (21.3 kB gzip)

Отключите эту возможность с помощью ppr: false в text.config.ts. Пререндеринг выполняет ваши компоненты во время сборки, поэтому не используйте глобальные объекты, доступные только в браузере, в коде верхнего уровня модулей. Маршрут, который выбрасывает исключение, вызывает redirect() / notFound() или выполняет переход во время пререндеринга, не получает оболочку (экран ошибки никогда не встраивается в HTML), а сборка сообщает причину.

Оболочка рендерится с размером viewport.width × viewport.height (по умолчанию 120 × 36), поскольку сборка не может знать размер консоли посетителя; затем гидратирующий кадр подстраивается под реальную консоль. Оболочка — встроенный <script>: если Content Security Policy запрещает встроенные скрипты, разрешите его по хешу или задайте ppr: false.

Развёртывание

text build записывает следующие файлы:

ФайлНазначение
index.html, <route>/index.htmlКлиентские маршруты с их оболочками. Работают на любом статическом хостинге.
assets/player-[hash].jsПроигрыватель кадров.
.text/server/live.mjsСерверные маршруты, App Router и рендерер консоли.
.text/pages/…, .text/live.htmlДокументы серверных маршрутов.
.text/ansi/…Первые кадры для curl и других терминальных клиентов.

text start и адаптер bun обслуживают все эти файлы. Серверным маршрутам нужен долгоживущий процесс: адаптеры static, vercel и cloudflare завершают сборку с ошибкой (TX030).

Изменить эту страницу на GitHub