सर्वर रेंडरिंग और आंशिक प्रीरेंडरिंग
runtime = "server": सर्वर पर रेंडर किए गए और फ़्रेम के रूप में स्ट्रीम किए गए रूट; प्रीरेंडर किए गए कंसोल शेल।
किसी रूट का React ट्री सर्वर पर रेंडर करें और उसके फ़्रेम कंसोल में स्ट्रीम करें। हर स्टैटिक रूट के पहले फ़्रेम की प्रीरेंडरिंग उसके HTML में करें।
दो रनटाइम
हर पेज दो में से किसी एक जगह रेंडर होता है:
| क्लाइंट (डिफ़ॉल्ट) | सर्वर (runtime = "server") | |
|---|---|---|
| React ट्री चलने की जगह | ब्राउज़र टैब | text dev / text start / bun अडैप्टर (Bun या Node) |
| ब्राउज़र का डाउनलोड | पूरा ऐप: React, Yoga WASM, आपके रूट | फ़्रेम प्लेयर, लगभग 21 kB gzip |
| डेटा ऐक्सेस | रूट हैंडलर को fetch() कॉल | सीधे: डेटाबेस, फ़ाइलें, node:*, सीक्रेट |
| एसिंक कंपोनेंट | use() या इफ़ेक्ट इस्तेमाल करें | async function कंपोनेंट का सपोर्ट है |
| कीस्ट्रोक लेटेंसी | उसी टास्क में, सिंक्रोनस | सर्वर तक एक राउंड ट्रिप |
| स्टैटिक होस्ट पर काम करता है | हां | नहीं, इसके लिए text start या bun dist/server.js चाहिए |
दोनों रनटाइम एक जैसे फ़्रेम बनाते हैं। फ़्रेम प्लेयर उसी CssConsoleDriver से सेल एनकोड करता है और उसी लगभग 15 fps की गति से चलता है, इसलिए सर्वर पर रेंडर किया गया फ़्रेम क्लाइंट की 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) का प्लेयर डाउनलोड करता है।
आप एक ऐप में अलग-अलग रनटाइम इस्तेमाल कर सकते हैं। उनके बीच नेविगेट करने पर टारगेट URL नए डॉक्यूमेंट के रूप में लोड होता है। ब्राउज़र बंडल हर सर्वर पेज की जगह एक स्टब रखता है, इसलिए सर्वर मॉड्यूल और उनके इंपोर्ट कभी क्लाइंट तक नहीं भेजे जाते।
सिर्फ़ सर्वर पर चलने वाले मॉड्यूल का नाम *.server.ts रखें। अगर ब्राउज़र तक भेजा जाने वाला कोड (क्लाइंट पेज या उसका इस्तेमाल किया हुआ लेआउट) कोई *.server.* मॉड्यूल या node:* बिल्ट-इन इंपोर्ट करता है, तो बिल्ड इंपोर्ट करने वाले का नाम बताते हुए फ़ेल होता है।
मिडलवेयर सर्वर रूट की सुरक्षा उसी तरह करता है जैसे डॉक्यूमेंट की: यह सेशन खुलने से पहले टारगेट रूट के लिए, सेशन के भीतर हर नेविगेशन (router.push, पीछे और आगे) से पहले, और सेशन की कुकी या Authorization बदलने पर फिर से चलता है। जब उसे रीडायरेक्ट, रिजेक्ट या री-राइट करना हो, तो ब्राउज़र इसके बजाय URL को डॉक्यूमेंट के रूप में लोड करता है और मिडलवेयर उस रिक्वेस्ट का जवाब देता है।
एसिंक सर्वर कंपोनेंट
सर्वर पर कोई भी async function कंपोनेंट अपना प्रॉमिस रिज़ॉल्व होने तक नज़दीकी <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} />;}हर प्रॉमिस प्रति सेशन कैश होता है और उसकी कुंजी कंपोनेंट और प्रॉप्स से बनती है, इसलिए दोबारा कोशिश करने वाला रेंडर पहले से चल रही रिक्वेस्ट का फिर इस्तेमाल करता है। रूट बदलने पर और router.refresh() कॉल पर कैश साफ़ होता है। इनपुट के बाद डेटा ऐक्सेस दोबारा चलाने के लिए router.refresh() कॉल करें:
const router = useRouter();useInput((e) => e.input === "r" && router.refresh());एसिंक कंपोनेंट को दिए गए प्रॉप्स सीरियलाइज़ किए जा सकने चाहिए: स्ट्रिंग, नंबर, साधारण ऑब्जेक्ट, ऐरे, Date, URL, Map, Set और एलिमेंट। फ़ंक्शन प्रॉप्स कुंजी का हिस्सा नहीं होते (डेटा पास करें, कॉलबैक नहीं)। हर सेशन ज़्यादा से ज़्यादा 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- हर टैब के लिए एक सेशन। हर पेज लोड अपना सेशन खोलता है: सर्वर पर एक React रूट, मेमोरी में राउटर हिस्ट्री और एक सेल बफ़र। सेशन ID 192-बिट के रैंडम मान होते हैं, जिन्हें ब्राउज़र सिर्फ़ मेमोरी में रखता है।
- सेल डेल्टा। सर्वर सिर्फ़ वे पंक्तियां भेजता है जिनका हैश उस कनेक्शन को मिले पिछले फ़्रेम के बाद बदला है। हर पंक्ति टेक्स्ट रन के रूप में भेजी जाती है, जिनकी स्टाइल ऐसी टेबल में होती हैं जिसमें सिर्फ़ नई एंट्री जोड़ी जाती हैं, और चौड़े ग्रैफ़ीम, इमोजी क्लस्टर और अर्धपारदर्शी रंग हूबहू बने रहते हैं।
- बैकप्रेशर। फ़्रेम के बीच कम से कम 66 ms का अंतर होता है, और हर फ़्रेम की तुलना सॉकेट के आख़िरी स्वीकार किए गए डेटा से होती है। जब धीमे कनेक्शन में डेटा निकलना बंद हो जाता है, तो बीच की स्थितियां छोड़ दी जाती हैं और अगला फ़्रेम सीधे सबसे नई स्थिति पर पहुंचता है।
- दोबारा कनेक्ट होना। स्ट्रीम टूटने पर प्लेयर
x-textjs-liveहेडर में अपनी सेशन ID के साथ दोबारा कनेक्ट होता है (URL में कभी नहीं) और नए कीफ़्रेम से उसी सेशन को उसकी स्थिति बरकरार रखते हुए फिर शुरू करता है। छूटे हुए अड्रेस बार अपडेट फिर चलाए जाते हैं। बिना कनेक्शन वाला सेशन रेंडर करना बंद कर देता है और 30 सेकंड बाद अनमाउंट हो जाता है; अगर सर्वर उसे भूल चुका है, तो प्लेयर नया सेशन खोलता है। - हिस्ट्री। पीछे और आगे दोनों हिस्ट्री को बराबर एंट्री से खिसकाते हैं, और सर्वर पर
router.back()ब्राउज़र की हिस्ट्री भी खिसकाता है। - सुरक्षा। दोनों एंडपॉइंट क्रॉस-साइट रिक्वेस्ट रिजेक्ट करते हैं (
Sec-Fetch-SiteऔरOrigin;X-Forwarded-Hostसिर्फ़server.trustProxyके साथ गिना जाता है)। इनपुट POST के लिए JSON बॉडी औरx-textjs-liveहेडर चाहिए। हर इवेंट की तय स्कीमा से जांच होती है, और नेविगेशन के टारगेट आपके ऑरिजिन के पाथ होने चाहिए। सिर्फ़ आपकेapp/मैनिफ़ेस्ट के पेज रेंडर हो सकते हैं। सेशन उन्हीं कुकी से बंधे होते हैं जिनके साथ वे खोले गए थे। - सीमाएं। हर POST में ज़्यादा से ज़्यादा 64 इवेंट और हर सेशन में लगभग 240 प्रति सेकंड (इसके बाद
429), बॉडी के लिए 5 सेकंड का टाइमआउट, हर क्लाइंट पते पर 32 सेशन, और हर प्रोसेस परmaxSessions(सबसे लंबे समय से डिस्कनेक्ट सेशन को पहले हटाया जाता है)।
export default defineConfig({ live: { maxSessions: 512, reconnectGraceMs: 60_000 },});आंशिक प्रीरेंडरिंग
text build हर स्टैटिक रूट को कॉन्फ़िगर किए गए viewport आकार पर एक बार रेंडर करता है और उस पहले फ़्रेम को छोटे क्लासिक <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)।