Build vs. Runtime in Webprojekten
Build vs. Runtime ist eine Grundentscheidung moderner Webarchitektur und beeinflusst Kosten, Geschwindigkeit und Komplexität.
Build vs. Runtime ist eine Grundentscheidung moderner Webarchitektur und beeinflusst Kosten, Geschwindigkeit und Komplexität. Sie klingt technisch, ist aber eine Produktentscheidung: Sie legt fest, wann eine Seite entsteht, wie frisch ihre Daten sind, was ein Fehler kostet und wie schnell ein Team liefern kann.
Warum ich das aufschreibe
In fast jedem Webprojekt taucht dieselbe Frage auf, meist zu spät: Rendern wir zur Build-Zeit oder zur Laufzeit? Häufig fällt die Antwort implizit – irgendein Framework-Default entscheidet, und ein halbes Jahr später wundert sich das Team über 20-Minuten-Builds oder über Server, die unter Last umkippen. Ich behandle den Zeitpunkt des Renderings deshalb nicht als Detail, sondern als Architekturgrenze. Wer diese Grenze bewusst zieht, verkürzt Zyklen, senkt Kosten und reduziert die Reibung zwischen Content, Entwicklung und Betrieb.
Die zwei Zeitpunkte
Jede Ausgabe im Web entsteht zu einem von zwei Zeitpunkten. Zur Build-Zeit läuft Arbeit einmal, bevor irgendein Nutzer ankommt: Templates werden zu HTML, Daten werden eingefroren, Assets werden gebündelt und optimiert. Zur Laufzeit läuft Arbeit pro Anfrage oder im Browser, während ein Nutzer wartet: Daten werden geholt, Rechte geprüft, HTML zusammengesetzt, Zustand hydriert.
Der entscheidende Unterschied ist nicht „schnell vs. langsam", sondern wer wartet und wie oft die Arbeit passiert. Build-Arbeit passiert einmal und wird beliebig oft ausgeliefert – der Nutzer wartet nie darauf. Runtime-Arbeit passiert bei jeder Anfrage erneut – dafür kennt sie den aktuellen Moment, den eingeloggten Nutzer und den letzten Datenstand.
flowchart LR
subgraph Build["Build-Zeit (einmal)"]
Content --> Prerender
Prerender --> StaticHTML["Static HTML + Assets"]
end
subgraph Runtime["Laufzeit (pro Request)"]
Request --> Fetch["Fetch data + auth"]
Fetch --> Render["Render + hydrate"]
end
StaticHTML --> CDN
CDN --> User
Render --> User
Zwischen diesen Polen liegt kein Entweder-oder, sondern ein Spektrum: Static Site Generation friert alles zur Build-Zeit ein. Server-Side Rendering baut pro Request neu. Client-Side Rendering schiebt die Arbeit in den Browser. Und Zwischenformen – prerender plus clientseitiges Nachladen, oder statisches HTML mit inkrementeller Revalidierung – mischen beide Zeitpunkte pro Route. Die Kunst liegt nicht darin, ein Verfahren zu wählen, sondern die Grenze pro Seite passend zu ziehen.
Der Trade-off, konkret
Ich mache den Trade-off an drei Größen fest, weil genau diese drei ein Produkt tragen oder bremsen: Frische der Daten, Kosten pro Anfrage und Time-to-Feedback im Team.
Eine Marketing- oder Doku-Seite ändert sich selten und ist für alle gleich. Sie zur Build-Zeit einzufrieren ist fast immer richtig: Sie liegt danach als statische Datei im CDN, kostet pro Aufruf praktisch nichts, hat keine Server-Latenz und keinen Server, der ausfallen kann.
// Build-time: runs once, per deploy
export async function getStaticProps() {
const posts = await cms.getPublishedPosts();
return { props: { posts } };
}
Ein Kontostand, ein personalisiertes Dashboard oder eine Preisliste, die sich stündlich ändert, verträgt kein Einfrieren. Diese Daten gehören an die Laufzeit – entweder pro Request auf dem Server oder als clientseitiger Fetch nach dem ersten Paint.
// Runtime: runs on every request, knows "now" and the user
export async function getServerSideProps(ctx) {
const user = await auth(ctx.req);
const balance = await ledger.getBalance(user.id);
return { props: { balance } };
}
Die eigentliche Frage lautet nicht „SSG oder SSR?", sondern: Wie lange darf diese Ausgabe alt sein, ohne falsch zu werden? Ich nenne das die Datenfrist. Ist die Frist praktisch unendlich (ein Blogpost von 2019), gehört die Seite in den Build. Ist die Frist null (der aktuelle Kontostand), gehört sie an die Laufzeit. Und dazwischen – die Frist beträgt Minuten oder Stunden – wird es interessant: Dann will ich die Auslieferungskosten eines statischen Assets, aber eine kontrollierte Auffrischung. Genau dafür existieren Caching mit Revalidierung und inkrementelle Regeneration. Sie sind kein Framework-Gimmick, sondern die technische Antwort auf eine endliche Datenfrist.
Was das messbar bewirkt
Der Grund, warum ich diese Grenze so ernst nehme, ist ihre Wirkung auf harte Zahlen.
Eine statisch ausgelieferte Seite antwortet mit der Latenz des nächsten CDN-Knotens – zweistellige Millisekunden, unabhängig von der Last. Dieselbe Seite pro Request serverseitig zu rendern bedeutet Datenbank-Roundtrips, Template-Rendering und einen Server, der mit dem Traffic skalieren muss. Bei einem Lastspitzen-Event ist das der Unterschied zwischen „das CDN liefert eine Datei" und „unsere Instanzen laufen voll und die Autoscaling-Rechnung explodiert". Für Seiten ohne echte Datenfrist ist Runtime-Rendering also nicht nur teurer, sondern auch fragiler – man kauft Komplexität, ohne einen Gegenwert zu bekommen.
Umgekehrt kostet zu viel Build-Arbeit Feedback. Wenn jede Änderung an einem Beitrag einen Full-Rebuild von zehntausend Seiten auslöst, wird der Build zum Nadelöhr: Ein Redaktions-Typo braucht 20 Minuten bis zur Sichtbarkeit, und niemand traut sich mehr, kurz vor Feierabend zu deployen. Das ist kein reines Performance-, sondern ein Produktivitätsproblem. Die Konsequenz ist nicht „mehr Runtime", sondern eine feinere Grenze: nur die betroffenen Seiten neu erzeugen, statt alles. Kürzere Feedback-Schleifen sind hier der eigentliche Gewinn – sie entscheiden, wie oft ein Team liefert und wie ruhig es das tut.
Ein Beispiel aus der Praxis, das mir mehrfach begegnet ist: Ein Content-lastiges Portal rendert historisch alles serverseitig, weil das mal der Framework-Default war. Unter normaler Last läuft es, aber die Server-Rechnung wächst linear mit dem Traffic, und jede Pressemeldung, die das Portal kurz viral gehen lässt, wird zum Incident. Verschiebt man die zwei Handvoll wirklich statischer Seitentypen – Artikel, Kategorien, Landingpages – in den Build und lässt nur Suche und personalisierte Bereiche an der Laufzeit, verändert sich die Kurve grundlegend. Der Löwenanteil des Traffics wird zur CDN-Datei, die Server tragen nur noch den kleinen dynamischen Rest, und die Lastspitze ist plötzlich ein Nicht-Ereignis. Es wurde keine Zeile Fachlogik umgeschrieben – nur die Grenze zwischen Build und Runtime an die tatsächliche Datenfrist angepasst.
Ich fasse die Entscheidung deshalb gern in einer kleinen Tabelle zusammen, bevor ich ein Tool anfasse:
| Seite | Datenfrist | Zeitpunkt |
|---|---|---|
| Landingpage, Docs | quasi unendlich | Build |
| Produktkatalog | Stunden | Build + Revalidierung |
| Suchergebnis, Feed | Sekunden | Runtime (Server) |
| Dashboard, Kontostand | null, personalisiert | Runtime (Server/Client) |
Die Tabelle ist bewusst schlicht. Sie erzwingt genau die eine Frage, die man sonst überspringt: Wie alt darf das hier sein?
Wie ich die Grenze ziehe
Ich beschreibe solche Themen in drei Schichten: fachliche Absicht, technische Grenze und betriebliche Konsequenz. Die Absicht erklärt, warum eine Seite existiert und für wen sie gleich oder verschieden aussieht. Die Grenze legt fest, was unabhängig geändert werden kann – hier fällt sie mit dem Rendering-Zeitpunkt zusammen. Die Konsequenz zeigt sich bei Last, Cache-Invalidierung, Rollback und Kosten.
Erst danach lohnt sich die Tool-Frage. Vorher frage ich: Ist diese Ausgabe für alle Nutzer gleich oder personalisiert? Wie lange darf sie alt sein? Was passiert, wenn die Datenquelle beim Rendern nicht antwortet? Und lässt sich der Zeitpunkt später ändern, ohne die halbe Anwendung umzubauen? Wenn ich eine Seite so schneide, dass ich ihren Rendering-Zeitpunkt isoliert verschieben kann – von Build zu Runtime oder zurück –, ohne Nachbarseiten anzufassen, dann ist die Grenze richtig gezogen.
Ein häufig unterschätzter Teil davon ist die Lebensdauer von Zustand. Im Frontend ist die schwierigste Entscheidung selten der Komponentenbaum, sondern die Frage, wo Zustand lebt: in einer Komponente, in der URL, in einem Cache, auf dem Server oder in einem lokalen Modell. Diese Frage ist die Build-vs-Runtime-Frage im Kleinen. Zustand, der in die URL gehört, kann serverseitig gerendert und geteilt werden. Zustand, der nur clientseitig existiert, verlangt Laufzeit im Browser und ist im statischen HTML schlicht nicht vorhanden. Wer diese Zuordnung sauber trifft, bekommt teilbare, indexierbare, testbare Seiten fast geschenkt.
Progressive Enhancement als dieselbe Grenze
Diese Denkweise ist nicht neu, sie ist nur alt geworden. Progressive Enhancement fragt seit jeher: Was liefere ich als robuste Basis, und was reichere ich zur Laufzeit an, wenn der Client es kann? Schon ein Canvas-Element trägt die Haltung in sich – erst prüfen, ob eine Fähigkeit da ist, dann ein Verhalten aktivieren, sonst einen Fallback anbieten.
<canvas id="clock" width="150" height="150">
<img src="clock.png" alt="Clock fallback" />
</canvas>
const canvas = document.getElementById("clock");
if (canvas.getContext) {
const ctx = canvas.getContext("2d");
// enhance at runtime, only when supported
}
Genau dieselbe Reihenfolge – Fähigkeit, Grenze, Verhalten – gilt für serverseitig geliefertes HTML plus clientseitige Hydration. Die statische Basis ist die Build-Ausgabe. Die Anreicherung ist die Laufzeit. Wenn beide Ebenen sauber getrennt sind, funktioniert die Seite auch dann, wenn die Laufzeit-Schicht langsam ist oder ausfällt. Das ist kein Legacy-Wissen, sondern die konzeptionelle Wurzel der ganzen Rendering-Diskussion.
Der Fehlerpfad entscheidet
Eine Rendering-Entscheidung ist erst belastbar, wenn ihr Fehlerpfad beschrieben ist. Was passiert, wenn das CMS beim Build nicht antwortet – bricht der Deploy ab oder geht die Seite mit Lücke live? Was passiert, wenn eine API beim serverseitigen Rendern hängt – blockiert die ganze Antwort, oder rendert die Seite ohne den Teil und lädt ihn clientseitig nach? Diese Fragen wirken trocken, sind aber der Unterschied zwischen Demo und Betrieb.
Build-Rendering verlagert das Ausfallrisiko in die Deploy-Pipeline: Fehler passieren einmal, sichtbar, vor dem Nutzer. Runtime-Rendering verteilt das Risiko auf jede einzelne Anfrage: Fehler passieren live, unter Last, im schlechtesten Moment. Beides ist legitim – aber man sollte wissen, welches Risiko man kauft. Ich prüfe deshalb immer, ob die Entscheidung rückbaubar ist: Kann ich eine Route von Runtime auf Build umstellen, wenn die Last steigt? Kann ich einen Cache gezielt invalidieren, ohne alles neu zu bauen? Wenn die Antwort nein ist, fehlt noch ein Teil der Architektur.
Typische Fehler
Der erste Fehler ist, das Werkzeug mit der Entscheidung zu verwechseln. Ein SSG-Framework ist keine Architektur, SSR ist kein Datenmodell, ein CDN ist keine Frische-Strategie. Diese Werkzeuge können die Grenze zwischen Build und Runtime sauber ausdrücken, aber sie ziehen sie nicht für dich. Wer den Default eines Frameworks übernimmt, hat keine Entscheidung getroffen, sondern eine ausgelassen.
Der zweite Fehler ist, den glücklichen Pfad zu überbewerten. Im Demo-Modus sieht jede Variante schnell aus – ein Datensatz, ein Nutzer, keine Last. Interessant wird es bei zehntausend Seiten, bei Traffic-Spitzen, bei einer langsamen Upstream-API, bei einem Redakteur, der eine Korrektur sofort sehen will. Genau dort zeigt sich, ob die Grenze zwischen Build und Runtime an der richtigen Stelle liegt.
Validierung
Drei Checks reichen mir meistens. Erstens: Für jede Route ist benannt, ob sie zur Build- oder zur Laufzeit entsteht – und warum, ausgedrückt als Datenfrist. Zweitens: Für jede Runtime-Route ist der Fehlerpfad beschrieben, inklusive des Verhaltens bei ausgefallener Datenquelle. Drittens: Die Entscheidung ist pro Route umstellbar, ohne Nachbarrouten anzufassen. Wenn diese drei Punkte stehen, ist die Architektur robust gegen genau die Überraschungen, die solche Projekte sonst spät und teuer einholen.
Weiterführende Quellen
- React: Thinking in React: https://react.dev/learn/thinking-in-react
- React: Managing State: https://react.dev/learn/managing-state
Die Quellen helfen beim Gegenprüfen von Standards und Plattformverhalten. Die eigentliche Aussage bleibt die Architekturentscheidung im konkreten Projekt.
Schluss
Build vs. Runtime ist keine Framework-Frage, sondern eine Frage nach der Datenfrist jeder einzelnen Seite. Wer sie bewusst pro Route beantwortet, bekommt statische Seiten dort, wo Frische egal ist – günstig, schnell, ausfallsicher – und dynamisches Rendering nur dort, wo es sich lohnt. Das Ergebnis ist nicht bloß eine schnellere Seite, sondern ein Team mit kürzeren Zyklen, geringeren Kosten und weniger Reibung zwischen Content, Code und Betrieb. Der Nutzen entsteht nicht im Toolnamen, sondern in der bewusst gezogenen Grenze.
Kommentare