Rendering di server dan prerender parsial

runtime = "server": route di-render di server dan dialirkan sebagai frame; shell konsol yang di-prerender.

Render pohon React sebuah route di server dan alirkan frame-nya ke konsol. Prerender frame pertama setiap route statis ke dalam HTML-nya.

Dua runtime

Setiap halaman di-render di salah satu dari dua tempat:

Klien (default)Server (runtime = "server")
Pohon React berjalan diTab browsertext dev / text start / adaptor bun (Bun atau Node)
Yang diunduh browserAplikasi lengkap: React, Yoga WASM, route AndaPemutar frame, sekitar 21 kB gzip
Akses datafetch() ke route handlerLangsung: basis data, file, node:*, rahasia
Komponen asinkronGunakan use() atau efekKomponen async function didukung
Latensi penekanan tombolTask yang sama, sinkronSatu perjalanan bolak-balik ke server
Berfungsi di host statisYaTidak, memerlukan text start atau bun dist/server.js

Kedua runtime menghasilkan frame yang sama. Pemutar frame mengodekan sel dengan CssConsoleDriver yang sama dan berjalan pada laju yang sama, sekitar 15 fps, sehingga frame yang di-render di server cocok dengan panggilan console.log klien byte demi byte.

Mengaktifkan

Ekspor runtime dari sebuah halaman:

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 dibaca per halaman sebagai ekspor string statis (anotasi tipe, as const, atau ekspor ulang diperbolehkan; nilai hasil komputasi tidak). Layout, loading.tsx, error.tsx, dan not-found.tsx di-render di tempat halamannya di-render: layout yang digunakan bersama oleh halaman klien dan server berjalan di kedua tempat dan dikirim ke browser, sementara layout dan boundary segmen yang hanya berisi halaman server tetap di server. Hook, fokus, useInput, <Link>, dan useRouter() berperilaku sama seperti di klien.

Memilih runtime

Gunakan runtime klien (default) untuk:

  • UI yang banyak melibatkan pengetikan, dengan setiap penekanan tombol harus digambar dalam task yang sama, seperti editor dan filter.
  • Aplikasi yang di-deploy ke host statis atau CDN.
  • Alat yang bisa digunakan secara offline.

Gunakan runtime server untuk:

  • Dasbor dan tampilan admin yang membaca basis data, file, antrean, atau API privat. Kredensial tetap di server.
  • Route dengan dependensi berat (SDK, parser, tabel sintaks) yang tidak Anda inginkan dalam bundel browser.
  • Halaman yang harus dimuat cepat di jaringan lambat. Browser mengunduh pemutar berukuran 21 kB (gzip), bukan aplikasi lengkap.

Anda bisa mencampur runtime dalam satu aplikasi. Navigasi di antaranya memuat URL tujuan sebagai dokumen baru. Bundel browser mengganti setiap halaman server dengan stub, sehingga modul server dan apa pun yang diimpornya tidak pernah dikirim ke klien.

Beri nama modul khusus server *.server.ts. Build gagal jika kode yang dikirim ke browser (halaman klien atau layout yang digunakan halaman klien) mengimpor modul *.server.* atau modul bawaan node:*, dengan menyebutkan pengimpornya.

Middleware melindungi route server dengan cara yang sama seperti melindungi dokumen: middleware berjalan untuk route tujuan sebelum sesi dibuka, sebelum setiap navigasi dalam sesi (router.push, Kembali dan Maju), dan setiap kali cookie sesi atau Authorization berubah. Saat middleware akan mengalihkan, menolak, atau menulis ulang, browser memuat URL sebagai dokumen, dan middleware menjawab permintaan tersebut.

Komponen server asinkron

Di server, setiap komponen async function menangguhkan rendering ke <Suspense> atau loading.tsx terdekat sampai promise-nya selesai:

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} />;}

Setiap promise disimpan dalam cache per sesi dengan kunci berdasarkan komponen dan props, sehingga render yang dicoba ulang menggunakan kembali permintaan yang sudah berjalan. Cache dikosongkan saat route berubah dan saat router.refresh() dipanggil. Untuk mengakses ulang data setelah input, panggil router.refresh():

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

Props yang diteruskan ke komponen asinkron harus bisa diserialisasi: string, angka, objek biasa, array, Date, URL, Map, Set, dan elemen. Props berupa fungsi bukan bagian dari kunci (teruskan data, bukan callback). Setiap sesi menyimpan paling banyak 256 promise dalam cache. Anda juga bisa membungkus komponen secara manual dengan serverComponent(fn) dari @textjs/app/server-components.

Hook (useState, useInput, …) tidak berfungsi di dalam komponen async function. Render komponen biasa di sekeliling atau di bawahnya untuk interaksi.

Cara kerja

browser tab ──(GET /__textjs/live, server-sent events)──▶ frame deltasbrowser tab ──(POST /__textjs/live, JSON)──────────────▶ keys, paste, wheel, resize, focus, Back/Forward
  • Satu sesi per tab. Setiap pemuatan halaman membuka sesinya sendiri: root React, riwayat router dalam memori, dan buffer sel di server. ID sesi adalah nilai acak 192-bit yang hanya disimpan browser dalam memori.
  • Delta sel. Server hanya mengirim baris yang hash-nya berubah sejak frame terakhir yang diterima koneksi tersebut. Setiap baris dikirim sebagai rangkaian teks yang merujuk ke tabel gaya yang hanya menerima penambahan di akhir, dan grafem lebar, kelompok emoji, serta warna translusen tetap dipertahankan persis.
  • Backpressure. Jarak antar-frame setidaknya 66 ms, dan setiap frame dibandingkan dengan data terakhir yang diterima soket. Saat koneksi lambat berhenti menguras data, status perantara dilewati, dan frame berikutnya langsung melompat ke status terbaru.
  • Koneksi ulang. Jika aliran terputus, pemutar menghubungkan ulang dengan ID sesinya di header x-textjs-live (tidak pernah di URL) dan melanjutkan sesi yang sama, dengan status tetap utuh, dari keyframe baru. Pembaruan bilah alamat yang terlewat diputar ulang. Sesi tanpa koneksi berhenti me-render dan dilepas setelah 30 detik; jika server melupakannya, pemutar membuka sesi baru.
  • Riwayat. Kembali dan Maju menggeser kedua riwayat dengan jumlah entri yang sama, dan router.back() di server juga menggeser riwayat browser.
  • Keamanan. Kedua endpoint menolak permintaan lintas situs (Sec-Fetch-Site dan Origin; X-Forwarded-Host hanya diperhitungkan dengan server.trustProxy). POST input memerlukan body JSON dan header x-textjs-live. Setiap event divalidasi terhadap skema tetap, dan tujuan navigasi harus berupa path pada origin Anda. Hanya halaman dalam manifes app/ Anda yang bisa di-render. Sesi terikat pada cookie yang digunakan saat sesi dibuka.
  • Batas. Paling banyak 64 event per POST dan sekitar 240 per detik per sesi (429 jika melebihi), batas waktu body 5 detik, 32 sesi per alamat klien, dan maxSessions per proses (sesi yang paling lama terputus dikeluarkan terlebih dahulu).
text.config.tsts
export default defineConfig({  live: { maxSessions: 512, reconnectGraceMs: 60_000 },});

Prerender parsial

text build me-render setiap route statis satu kali, pada ukuran viewport yang dikonfigurasi, dan menyisipkan frame pertama tersebut langsung ke HTML route sebagai <script> klasik berukuran kecil. Konsol menggambar sebelum modul apa pun dimuat:

  1. Shell statis. Semua yang di-render secara sinkron. Setiap boundary <Suspense> yang masih menunggu menampilkan fallback-nya, termasuk komponen server asinkron (yang tidak berjalan saat shell di-render), use(), dan modul lazy. fetch() tidak pernah selesai saat shell di-render, sehingga data yang dimuat dalam efek juga tetap dalam status pemuatan.
  2. Celah dinamis. Saat JavaScript mulai berjalan, renderer klien atau pemutar frame mengalirkan frame yang sebenarnya. Frame ditambahkan di akhir, di bawah shell, dan tirai milik frame yang melakukan hidrasi menggeser shell keluar dari pandangan dalam proses penggambaran DevTools yang sama. Shell mencatat waktu penggambarannya, sehingga frame hidrasi pertama menunggu hingga jendela pascapembersihan DevTools berakhir alih-alih masuk ke jalur lambatnya.

Selama pemuatan, hanya shell yang membersihkan konsol. Renderer yang melakukan hidrasi dimulai dalam mode menambahkan di akhir (initialClear: false), sehingga tidak ada kedipan, dan shell hanya menggambar pada path yang menjadi tujuan render-nya. Route dinamis ([id], force-dynamic) tidak me-render shell.

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

Nonaktifkan dengan ppr: false di text.config.ts. Prerender menjalankan komponen Anda saat build, jadi pastikan kode tingkat atas modul bebas dari variabel global khusus browser. Route yang melempar error, memanggil redirect() / notFound(), atau melakukan navigasi saat prerender tidak mendapat shell (layar error tidak pernah disematkan ke HTML), dan build memberi tahu alasannya.

Shell di-render pada ukuran viewport.width × viewport.height (default 120 × 36) karena build tidak bisa mengetahui ukuran konsol pengunjung; frame yang melakukan hidrasi kemudian menyesuaikan ukuran dengan konsol sebenarnya. Shell berupa <script> inline: jika Content Security Policy melarang skrip inline, izinkan melalui hash atau tetapkan ppr: false.

Deploy

text build menulis file berikut:

FileTujuan
index.html, <route>/index.htmlRoute klien beserta shell-nya. Berfungsi di host statis mana pun.
assets/player-[hash].jsPemutar frame.
.text/server/live.mjsRoute server, App Router, dan renderer konsol.
.text/pages/…, .text/live.htmlDokumen route server.
.text/ansi/…Frame pertama untuk curl dan klien terminal lainnya.

text start dan adaptor bun menyajikan semuanya. Route server memerlukan proses yang berjalan lama: adaptor static, vercel, dan cloudflare menggagalkan build (TX030).

Edit halaman ini di GitHub