サーバーレンダリングと部分的なプリレンダリング
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 をエクスポートします。
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 でサスペンドします。
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です(切断時間が最も長いセッションから削除されます)。
export default defineConfig({ live: { maxSessions: 512, reconnectGraceMs: 60_000 },});部分プリレンダリング
text build は、設定された viewport サイズですべての静的ルートを1回ずつレンダリングし、最初のフレームを小さなクラシック <script> としてルートのHTMLに埋め込みます。モジュールが読み込まれる前にコンソールが描画されます。
- 静的シェル。 同期的にレンダリングされるすべての内容です。待機中のすべての
<Suspense>境界はフォールバックを表示します。これには非同期サーバーコンポーネント(シェルのレンダリング中は実行されません)、use()、遅延読み込みモジュールが含まれます。シェルのレンダリング中にfetch()が解決することはないため、エフェクトで読み込むデータも読み込み中の状態を維持します。 - 動的な領域。 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)。