← Blog

Renderizado en el servidor en la consola

Text.js 0.4 puede renderizar una página en el servidor y transmitir sus fotogramas a la consola como diferencias entre celdas. Las rutas estáticas también prerenderizan su primer fotograma en el HTML.

Adam

Text.js 0.4 añadió un segundo lugar donde renderizar una página. Por defecto, el árbol de React de una página se ejecuta en la pestaña del navegador. Una página que exporta runtime = "server" se ejecuta en el servidor, y el navegador recibe sus fotogramas.

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 es un componente async function. En el servidor, suspende su renderizado en el <Suspense> o loading.tsx más cercano hasta que se resuelve su promesa, y puede leer archivos, bases de datos y módulos node:* directamente. Los componentes asíncronos cargan datos y no pueden usar hooks. Los componentes normales que los rodean conservan los hooks, el foco, useInput, <Link> y useRouter(), que se comportan igual en ambos entornos de ejecución.

Lo que descarga el navegador

Una página renderizada en el servidor no envía React, Yoga ni tus rutas. El navegador carga un reproductor de fotogramas de unos 21 kB con gzip, que contiene un CellBuffer y el mismo CssConsoleDriver que usa el renderizador del cliente. Mantiene la misma cadencia de 15 fps, por lo que un fotograma renderizado en el servidor produce la misma llamada a console.log que produciría el cliente.

El protocolo

Cada pestaña abre su propia sesión en el servidor: una raíz de React, un historial del enrutador en memoria y un búfer de celdas. Un único endpoint gestiona ambas direcciones:

browser tab ──(GET /__textjs/live, server-sent events)──▶ frame deltasbrowser tab ──(POST /__textjs/live, JSON)──────────────▶ keys, paste, wheel, resize, focus, Back/Forward

El servidor solo envía las filas cuyo hash ha cambiado desde el último fotograma que recibió esa conexión. Cada fila se envía como segmentos de texto asociados a una tabla de estilos que solo permite añadir entradas al final. Los grafemas anchos, las agrupaciones de emojis y los colores translúcidos llegan intactos.

Los fotogramas se separan al menos 66 ms, y cada uno se compara con lo último que aceptó el socket. Cuando una conexión lenta deja de vaciar su búfer, se omiten los estados intermedios y el siguiente fotograma salta al estado más reciente.

Si se interrumpe el flujo, el reproductor vuelve a conectarse con su identificador de sesión en la cabecera x-textjs-live y reanuda la misma sesión desde un nuevo fotograma clave, con su estado intacto. Los identificadores de sesión son valores aleatorios de 192 bits que se guardan en memoria, nunca en la URL. Una sesión sin conexión deja de renderizar y se desmonta tras 30 segundos.

Ambos endpoints rechazan las solicitudes entre sitios, y cada evento de entrada se valida con un esquema fijo. Una sesión acepta como máximo 64 eventos por POST y unos 240 por segundo.

Componentes asíncronos y datos

La promesa de cada componente asíncrono se almacena en caché por sesión, con una clave basada en el componente y sus props, de modo que un nuevo intento de renderizado reutiliza la solicitud que ya está en curso. La caché se vacía cuando cambia la ruta y cuando llamas a router.refresh():

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

Los módulos exclusivos del servidor se nombran *.server.ts. La compilación falla si el código que se envía al navegador importa uno de ellos o un módulo integrado node:*, e indica qué módulo lo importa.

Prerenderizado parcial

El prerenderizado parcial funciona sin servidor. Ahora text build renderiza cada ruta estática una vez e inserta su primer fotograma en el HTML de la ruta como un pequeño <script> clásico, para que la consola se pinte antes de que se cargue cualquier módulo.

La estructura inicial es todo lo que se renderiza de forma síncrona. Un límite <Suspense> que sigue esperando muestra su contenido alternativo, incluidos los componentes asíncronos, y fetch() nunca se resuelve mientras se renderiza la estructura inicial.

Cuando se inicia JavaScript, el renderizador del cliente o el reproductor de fotogramas transmite el fotograma real. Como 0.4 también añade fotogramas al final en lugar de borrar, el renderizador que realiza la hidratación se inicia con initialClear: false y escribe debajo de la estructura inicial, y su cortina desplaza esa estructura fuera de la vista en el mismo repintado de DevTools. La consola se borra exactamente una vez, a cargo de la estructura inicial.

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

La estructura inicial se renderiza con el tamaño de viewport configurado, 120 × 36 por defecto, porque la compilación no puede conocer el tamaño de la consola del visitante. Con viewport.responsive activado, el primer fotograma en vivo se ajusta al tamaño de la consola acoplada. Las rutas dinámicas no renderizan ninguna estructura inicial, y ppr: false en text.config.ts desactiva el prerenderizado.

Elegir un entorno de ejecución

El entorno de ejecución del cliente sigue siendo el predeterminado. Las pulsaciones de teclas se pintan en la misma tarea, y la app se despliega en cualquier alojamiento estático. El entorno de ejecución del servidor es adecuado para paneles y vistas de administración que leen datos privados, y para rutas cuyas dependencias no quieres incluir en el paquete del navegador. Una app puede combinar ambos, y navegar entre ellos carga la URL de destino como un documento nuevo.

La referencia completa está en Renderizado en el servidor.