サーバーレンダリングと部分的なプリレンダリング

runtime = "server":サーバーで描画してフレームとしてストリーミングするルートと、プリレンダリングしたコンソールシェル。

ルートのReactツリーをサーバーでレンダリングし、そのフレームをコンソールにストリーミングします。すべての静的ルートの最初のフレームをHTML内にプリレンダリングします。

2つのランタイム

各ページは、次のいずれかの場所でレンダリングされます。

クライアント(デフォルト)サーバー(runtime = "server")
Reactツリーの実行場所ブラウザーのタブtext dev / text start / bun アダプター(BunまたはNode)
ブラウザーのダウンロード対象アプリ全体:React、Yoga WASM、ルートフレームプレーヤー、gzipで約21 kB
データアクセスルートハンドラーへの fetch()直接アクセス:データベース、ファイル、node:*、シークレット
非同期コンポーネントuse() またはエフェクトを使用async function コンポーネントに対応
キー入力のレイテンシー同一タスク内で同期実行サーバーとの1往復
静的ホストでの動作対応非対応、text start または bun dist/server.js が必要

どちらのランタイムも同じフレームを生成します。フレームプレーヤーは同じ CssConsoleDriver でセルをエンコードし、同じ約15fpsのペースで動作するため、サーバーでレンダリングしたフレームはクライアントの 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() はクライアントと同じように動作します。

ランタイムの使い分け

次の場合は、クライアントランタイム(デフォルト)を使用します。

  • エディターやフィルターなど、キー入力のたびに同一タスク内で描画する必要がある、入力の多いUI。
  • 静的ホストやCDNにデプロイするアプリ。
  • オフラインで利用できるツール。

次の場合は、サーバーランタイムを使用します。

  • データベース、ファイル、キュー、プライベートAPIを読み取るダッシュボードや管理画面。認証情報はサーバーに保持されます。
  • ブラウザーバンドルに含めたくない大きな依存関係(SDK、パーサー、構文テーブル)を持つルート。
  • 低速なネットワークでも高速に読み込む必要があるページ。ブラウザーはアプリ全体ではなく21 kB(gzip)のプレーヤーをダウンロードします。

1つのアプリで複数のランタイムを併用できます。異なるランタイム間を移動すると、移動先のURLが新しいドキュメントとして読み込まれます。ブラウザーバンドルでは各サーバーページがスタブに置き換えられるため、サーバーモジュールとそのインポート先がクライアントに配信されることはありません。

サーバー専用モジュールには *.server.ts という名前を付けます。ブラウザーに配信されるコード(クライアントページ、またはクライアントページが使用するレイアウト)が *.server.* モジュールや node:* 組み込みモジュールをインポートすると、ビルドは失敗し、インポート元が示されます。

ミドルウェアは、ドキュメントと同じようにサーバールートを保護します。セッション開始前、セッション内の各ナビゲーション(router.push、「戻る」、「進む」)の前、さらにセッションのCookieや Authorization が変わるたびに、対象ルートに対して実行されます。リダイレクト、拒否、書き換えが必要な場合は、代わりにブラウザーがURLをドキュメントとして読み込み、ミドルウェアがそのリクエストに応答します。

非同期サーバーコンポーネント

サーバーでは、すべての async function コンポーネントが、Promiseが解決するまで最も近い <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} />;}

各Promiseはセッションごとに、コンポーネントとpropsをキーとしてキャッシュされるため、レンダリングを再試行すると、すでに進行中のリクエストが再利用されます。キャッシュはルート変更時と router.refresh() の実行時にクリアされます。入力後にデータアクセスを再実行するには、router.refresh() を呼び出します。

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

非同期コンポーネントに渡すpropsは、文字列、数値、プレーンオブジェクト、配列、Date、URL、Map、Set、要素など、シリアライズ可能な値にしてください。関数のpropsはキーに含まれません(コールバックではなくデータを渡してください)。各セッションでキャッシュできるPromiseは最大256個です。@textjs/app/server-components の serverComponent(fn) でコンポーネントを手動でラップすることもできます。

フック(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
  • タブごとに1セッション。 ページを読み込むたびに専用のセッションが開かれ、サーバー上にReactルート、メモリー内のルーター履歴、セルバッファーが作成されます。セッションIDは192ビットのランダムな値で、ブラウザーはメモリー内にのみ保持します。
  • セルの差分。 サーバーは、その接続が最後に受信したフレーム以降にハッシュが変わった行だけを送信します。各行は追記専用のスタイルテーブルを参照するテキストの連続区間として送信され、幅の広い書記素、絵文字クラスター、半透明の色も正確に維持されます。
  • バックプレッシャー。 フレームの間隔は最低66msで、各フレームはソケットが最後に受け付けた内容との差分が計算されます。低速な接続で送信待ちデータが流れなくなると、中間の状態は省略され、次のフレームは最新の状態に直接進みます。
  • 再接続。 ストリームが切断されると、プレーヤーは x-textjs-live ヘッダーにセッションIDを入れて(URLには含めずに)再接続し、状態を維持したまま、新しいキーフレームから同じセッションを再開します。受信できなかったアドレスバーの更新は再適用されます。接続のないセッションはレンダリングを停止し、30秒後にアンマウントされます。サーバーがそのセッションを保持していなければ、プレーヤーは新しいセッションを開きます。
  • 履歴。 「戻る」と「進む」は両方の履歴を同じエントリー数だけ移動し、サーバー上の router.back() もブラウザーの履歴を移動します。
  • セキュリティ。 両エンドポイントはクロスサイトリクエストを拒否します(Sec-Fetch-Site と Origin を使用し、X-Forwarded-Host は server.trustProxy が有効な場合のみ考慮されます)。入力のPOSTにはJSON本文と x-textjs-live ヘッダーが必要です。すべてのイベントは固定スキーマで検証され、移動先は同一オリジンのパスでなければなりません。レンダリングできるのは app/ マニフェスト内のページのみです。セッションは開始時のCookieに紐付けられます。
  • 制限。 POSTあたり最大64イベント、セッションあたり毎秒約240イベント(超過時は 429)、本文のタイムアウトは5秒、クライアントアドレスあたり32セッション、プロセスあたり maxSessions です(切断時間が最も長いセッションから削除されます)。
text.config.tsts
export default defineConfig({  live: { maxSessions: 512, reconnectGraceMs: 60_000 },});

部分プリレンダリング

text build は、設定された viewport サイズですべての静的ルートを1回ずつレンダリングし、最初のフレームを小さなクラシック <script> としてルートのHTMLに埋め込みます。モジュールが読み込まれる前にコンソールが描画されます。

  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)

無効にするには、text.config.ts に ppr: false を設定します。プリレンダリングではビルド時にコンポーネントを実行するため、モジュールのトップレベルのコードでブラウザー専用のグローバル変数を使用しないでください。プリレンダリング中に例外をスローしたり、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でこのページを編集