Jede Seite entscheidet,
wie sie rendert.
Der Render-Modus ist eine Entscheidung pro Seite, nicht pro Projekt. Eine Marketing-Seite geht als statisches HTML ohne JavaScript raus, das Dashboard daneben hydriert vollständig, und eine Pricing-Seite revalidiert auf einem Timer — selber Router, selbe Loader, eine Export-Zeile Unterschied.
// src/pages/pricing/page.tsx — static HTML, zero JS on the wire
export const renderMode = 'static'
export const meta = ({ locale }) => ({ title: t(locale, 'pricing.title') })
// src/pages/app/page.tsx — same router, fully interactive
export const renderMode = 'spa'
// src/pages/blog/[slug]/page.tsx — pre-rendered, then revalidated
export const renderMode = 'isr'
export const revalidate = 600Der Router, konkret.
Eine Datei ist eine Route
Eine page.tsx ist eine Seite, eine layout.tsx umschließt alles darunter, und ein [param]-Verzeichnis fängt ein Segment. Keine Route-Tabelle, die mit dem Dateisystem synchron gehalten werden muss, und nichts zu registrieren — eine Datei zu verschieben verschiebt die URL.
Fünf Render-Modi, ein Export
static (beim Build vorgerendert), ssr (pro Request), isr (vorgerendert, dann revalidiert), spa (nur Client), islands (statisches HTML mit interaktiven Inseln) — und Partial Prerendering auf isr: eine gecachte anonyme Shell mit Löchern pro Request dahinter, Subject-Daten erreichen den Cache nie. Eine Seite änderst du, indem du einen anderen renderMode exportierst; Loader, Daten und Komponenten bleiben, wo sie sind.
Null JS, wo eine Seite keins deklariert
Eine Seite, die Interaktivität abwählt, liefert HTML und sonst nichts — der Zero-JS-Modus entfernt jedes Script und jeden Preload aus der Ausgabe. Islands machen Interaktivität explizit UND billig: eine Islands-Seite liefert ihren eigenen schlanken Entry — react plus Island-Runtime plus ihre Islands, nicht das App-Bundle — gemessen unter einem Drittel des First Load einer voll hydrierten Seite, während der Rest der Seite server-gerendertes HTML bleibt, das nie neu rendert.
Loader, die end-to-end typisiert sind
Ein Page-Loader läuft auf dem Server und sein Rückgabetyp fließt in die Komponente — kein manuelles Generic, kein Cast. Er kann deine api über dieselben typisierten Procedures aufrufen wie der Client, First Paint und spätere Navigation lesen also denselben Vertrag.
Metadaten, die die Locale kennen
Title, Description, Canonical, OG-Tags und JSON-LD werden pro Route mit der aktiven Locale berechnet — eine übersetzte Seite bekommt also schon zur Build-Zeit übersetzte Head-Tags, genau die Version, die eine Suchmaschine indexiert. Und das og:image selbst wird generiert: als JSX-Template deklariert, backen statische Seiten das PNG beim Build, während ssr-Seiten es on demand über eine signierte, gecachte Route liefern.
Navigation ohne Boilerplate
Client-seitige Navigation, ein Navigations-Indikator, Scroll-Restoration, Prefetch-on-Intent und Opt-in-View-Transitions (der Cross-Fade des Browsers, gestylt mit purem CSS) sind verdrahtet. Anchor-Klicks werden abgefangen, wo es hilft, und in Ruhe gelassen, wo nicht — ein externer Link bleibt ein externer Link.
Modal-mit-URL (Intercepting Routes)
Ein Page-Export — intercept: { from: '/photos' } — und eine Soft-Navigation öffnet das Detail als Overlay über der weiterhin gemounteten Liste (getippte Filter, Scroll und State unangetastet), während Reload oder geteilter Link dieselbe URL standalone rendert. Zurück schließt es, der Blocker eines dirty Forms hält jetzt auch die Zurück-Geste, und jeder Baum behält seinen eigenen Query-String. Parallele @slot-Routen sind ein deklariertes Nicht-Ziel: Split-Panes sind Komponenten im Layout, kein Routing-Konzept.
Warum der Modus pro Seite gilt.
Eine App ist selten eine einzige Last
Die meisten Frameworks lassen dich eine Rendering-Strategie fürs Projekt wählen — und danach kämpfst du an den Rändern damit: an einen Static-Site-Generator wird ein Dashboard geschraubt, oder eine server-gerenderte App rendert Marketing-Seiten über ein Plugin vor. Echte Produkte sind beides gleichzeitig. Den Modus zu einem Export pro Seite zu machen heißt: die Marketing-Seite behält ihre Zero-JS-Payload im selben Deploy wie die voll interaktive App — ohne zweite Toolchain und ohne zweites mentales Modell.
Eine Datei ist eine Route. Vier davon.
// src/pages/pricing/page.tsx → /pricing
export default function Pricing() {
return <PricingTable />
}Der Render-Modus ist eine Export-Zeile, und es ist die Zeile, die man kennen sollte: dieselbe Seite kann statisch, serverseitig oder rein clientseitig sein, ohne dass sich sonst etwas an ihr ändert. Du entdeckst nicht beim Launch, dass Marketing-Site und App zwei Toolchains brauchen.
Der Router im Detail.
Wie funktioniert dateibasiertes Routing hier?
Eine `page.tsx` ist eine Route, eine `layout.tsx` umschließt alles darunter, und ein `[param]`-Verzeichnis fängt ein URL-Segment. Es gibt keine Route-Tabelle, die mit dem Dateisystem Schritt halten müsste, und nichts zu registrieren — eine Datei zu verschieben verschiebt die URL, eine zu löschen löscht die Route.
Route-Parameter werden aus dem Pfad erschlossen — eine Seite, die einen Parameter liest, den ihre Route nicht deklariert, kompiliert nicht. Das schließt die Lücke, in der ein Refactoring ein Segment umbenennt und die Komponente den alten Namen weiterliest, bis jemand den richtigen Link klickt. Der Query-String bekommt dieselbe Behandlung: eine Seite deklariert einmal ein Search-Param-Schema, Reads kommen dekodiert und mit Defaults zurück, und Links auf die Route typprüfen ihre Params dagegen.
Layouts verschachteln sich, gemeinsame Chrome wird also einmal auf der Ebene deklariert, für die sie gilt, statt in jede Seite darunter importiert zu werden — und ein Layout kann eigene Daten laden, was eine Sidebar davon abhält, bei jeder Navigation neu zu laden.
Warum ist der Render-Modus eine Entscheidung pro Seite?
Weil eine Anwendung selten eine einzige Last ist. Eine Marketing-Seite will vorgerendertes HTML ohne JavaScript; das Dashboard daneben will volle Interaktivität; eine Pricing-Seite will Vorrendern mit periodischer Revalidierung. Allen dreien eine Strategie aufzuzwingen heißt, bei zweien dagegen zu kämpfen.
Hier ist es ein Export. Ändere `renderMode`, und Loader, Daten und Komponenten bleiben exakt, wo sie sind — das macht die Wahl billig zu revidieren, wenn sich eine Seite als dynamischer oder statischer herausstellt als angenommen.
Der Gewinn ist konkret: eine Zero-JS-Seite liefert HTML und sonst nichts — kein Hydrations-Bundle, keine Framework-Runtime auf der Leitung — während die interaktive App im selben Deployment sich wie eine normale SPA verhält. Islands liegen dazwischen: die Seite liefert einen schlanken Per-Page-Entry (react + ihre Islands, kein App-Bundle), die markierten Widgets hydrieren, der Rest bleibt statisches HTML.
Wie funktionieren Loader und Metadaten?
Ein Loader läuft auf dem Server, und sein Rückgabetyp fließt in die Komponente — kein manuelles Generic, kein Cast. Er kann deine api über dieselben typisierten Procedures rufen wie der Client, die Daten beim ersten Paint und nach der Navigation kommen also aus einem Vertrag statt aus zweien.
Metadaten werden pro Route mit der aktiven Locale berechnet — Titel, Descriptions, Canonicals, OG-Tags und JSON-LD stimmen also in dem HTML, das ein Crawler liest, übersetzte Varianten eingeschlossen. Das ist die Hälfte von SEO, die eine reine Client-Bibliothek strukturell nicht leisten kann.
Für eine abonnierende Seite leert der Loader die Query über einen One-Shot-Pfad mit derselben Auth-Middleware bis zum ersten Snapshot, und die Live-Subscription übernimmt nach der Hydration. Erstes Rendern und Live-Updates sind dieselbe Query, kein doppelter Fetch.
Was muss ich nicht bauen?
Client-seitige Navigation, einen Navigations-Indikator, Scroll-Restoration, Prefetch on Intent, Opt-in-View-Transitions mit Reduced-Motion-Fallback, Anchor-Interception, die externe Links in Ruhe lässt, Modal-mit-URL-Intercepting-Routes — und eine Build-Time-Bild-Pipeline: ein Import-Suffix macht aus einem statischen Asset AVIF/WebP-srcSet-Varianten mit inferierten Dimensionen und Blur-Placeholder. Jedes für sich ist klein; zusammen sind sie der Unterschied zwischen einem Router und einem Projekt.
Die Entwicklung bekommt Hot-Updates auf Routenebene statt eines vollen Reloads bei jeder Bearbeitung — Komponenten-State überlebt also, während du an einer Seite iterierst. Eine Eigenschaft, die still darüber entscheidet, wie angenehm ein Nachmittag UI-Arbeit ist.
Und die Grenze wird erzwungen statt geglaubt: alles, was die Codegen ins Browser-Bundle zieht, muss browser-sicher sein, und ein Boot-Guard bricht mit der exakten Import-Kette ab, wenn ein Descriptor über transitive Importe ein serverseitiges Modul hereinziehen würde.
Die Render-Modi und wann welcher passt.
| Modus | Nimm ihn, wenn |
|---|---|
| static | Die Seite vorab baubar ist — vorgerendertes HTML, mit Zero-JS-Opt-out, das gar kein Script liefert. |
| islands | Überwiegend statisch mit interaktiven Inseln — die Seite liefert ihren eigenen schlanken Entry (react + ihre Islands, nicht das App-Bundle), und nur die markierten Islands hydrieren. |
| ssr | Die Antwort vom Request abhängt: personalisiert, authentifiziert oder pro Besuch frisch berechnet. |
| isr | Vorgerendert und auf drei Wegen revalidiert: per Timer, per Change-Data-Capture bei Änderungen einer beobachteten Tabelle, oder on demand — revalidatePath/revalidateTable/revalidateTag aus Server-Code erreichen jede Replica, mit oder ohne CDC. Mit ppr = true bekommt die gecachte Shell Löcher pro Request: deferred Felder streamen auf dieselbe Response, fail-closed gegen Credentials in der Shell. |
| spa | Der Zustand im Browser lebt und ein server-gerenderter erster Paint bei der Hydration verworfen würde. |
| Loader | Serverseitige Daten, deren Rückgabetyp in die Komponente fließt; leert eine Subscription für den ersten Paint. |
Häufige Fragen
Kann ein Projekt statische und interaktive Seiten mischen?
Ja — genau dafür ist der Modus ein Export pro Seite. Eine Zero-JS-Marketing-Seite und ein voll interaktives Dashboard gehen im selben Deployment raus, aus einem Router, mit einem Build und einem mentalen Modell.
Liefern statische Seiten wirklich kein JavaScript?
Wenn die Seite Interaktivität abwählt, ja: der Zero-JS-Modus entfernt jedes Script und jeden Preload, es bleibt pures HTML. `islands` hält die Seite als statisches HTML, hydriert nur die markierten Widgets und liefert einen schlanken Per-Page-Entry statt des App-Bundles — gemessen unter einem Drittel des First-Load-JavaScripts einer vollen Seite.
Woher bekommen Loader ihre Typen?
Aus ihrem eigenen Rückgabewert — der Typ fließt in die Komponente, ohne Generic und ohne Cast. Ruft ein Loader deine api, geht er durch dieselben typisierten Procedures wie der Client, eine Signaturänderung bricht also auch den Loader.
Werden SEO-Metadaten für übersetzte Seiten behandelt?
Ja. Metadaten werden pro Route mit der aktiven Locale berechnet — eine übersetzte Seite hat übersetzte Head-Tags, Canonicals und strukturierte Daten in dem HTML, das ein Crawler liest, nicht nach der Hydration eingefügt.
Was verhindert, dass Server-Code im Browser-Bundle landet?
Ein Boot-Guard, der den generierten Import-Graphen abläuft und mit der exakten Kette abbricht, wenn ein Descriptor transitiv ein serverseitiges Modul erreicht. Die Falle ist transitiv — ein geteilter Helfer, der auch den Datenbank-Handle importiert — deshalb wird es strukturell erzwungen statt per Review.
Woran Routing hängt.
Öffne das Framework. Schau es dir selbst an.
Jede Primitive auf dieser Seite ist heute im Framework. Klone den Starter, lass `voltro dev` laufen, in zwei Minuten ist es auf dem Bildschirm.