Eine Seite ohne JavaScript aktualisieren
Übertragen Sie HTML-Updates ohne JavaScript mit template for und Verarbeitungsanweisungen. Mit Node-Beispiel, Browserunterstützung, Fallbacks und Grenzen.
Ein <template for="name">-Element füllt einen Platzhalter, der weiter oben im selben Dokument durch eine Verarbeitungsanweisung (Processing Instruction) mit diesem Namen markiert wurde. Der Server sendet die langsamen Abschnitte am Ende derselben HTML-Antwort, und der Parser des Browsers verschiebt sie an die richtige Stelle. Dabei wird kein Skript ausgeführt.
Die meisten serverseitig gerenderten Seiten haben einen langsamen Bereich, etwa eine Bestellanzahl oder eine Statusprüfung. Die übliche Lösung ist ein Lade-div plus ein Skript, das das Fragment nachträglich abruft. Genau dieses Skript würden viele Teams gern loswerden.
Dieser Artikel zeigt das Markup, einen lauffähigen Node-Server, der die Updates streamt, wo sich das Muster im Vergleich zu htmx einordnet und was Browser ohne Unterstützung darstellen.
Das Wichtigste in Kürze
<template for>ersetzt einen Bereich<?start name="x">…<?end>oder füllt einen Einfügepunkt<?marker name="x">, sobald der HTML-Parser es erreicht.- Der Server muss zuerst das Seitengerüst (Shell) flushen und die Templates später in derselben Antwort schreiben. Jede Schicht, die die komplette Antwort puffert, liefert Platzhalter und Ersatz gleichzeitig aus.
- Chrome 150 aktiviert Out-of-Order-Streaming standardmäßig. Die zugehörigen JavaScript-Methoden wie
streamHTMLfolgen in Chrome 155. Ältere Versionen benötigen ein Flag. - Browser ohne Unterstützung parsen die Verarbeitungsanweisungen als Kommentare, sodass der Fallback-Inhalt sichtbar bleibt.
Das Problem: Abrufen, Finden, Austauschen
Bisher erforderte die Aktualisierung eines einzelnen Bereichs einer serverseitig gerenderten Seite drei clientseitige Schritte: ein Fragment abrufen, den Zielknoten finden und dessen Inhalt austauschen. Selbst diese minimale Variante braucht ein Skript, einen zweiten Request und eine DOM-Abfrage:
<div id="status">Loading…</div>
<script type="module">
const res = await fetch('/status.html');
document.querySelector('#status').innerHTML = await res.text();
</script>
Der Server kannte den Status bereits, während er die erste Antwort renderte. Der zusätzliche Roundtrip existiert nur, weil HTML schon immer in Dokumentreihenfolge geparst wurde.
Wie deklarative Teilaktualisierungen im Markup funktionieren
Deklarative Teilaktualisierungen (Declarative Partial Updates) verwenden Verarbeitungsanweisungen als benannte Anker und <template for> als Patch. Chromes Ankündigung zu Declarative Partial Updates definiert drei Anker: <?marker name="x"> ist ein Einfügepunkt. <?start name="x"> und <?end> umschließen einen Bereich, der ersetzt wird.
Soll der Platzhalter Fallback-Inhalt anzeigen, verwenden Sie ein <?start>…<?end>-Paar. Sobald das passende Template eintrifft, wird alles zwischen den beiden Anweisungen ersetzt. Ein einzelner <?marker> markiert nur einen Punkt, Fallback-Text daneben wird also nie entfernt.
<div id="status"><?start name="status">Loading…<?end></div>
<!-- later in the same response -->
<template for="status">All systems <strong>normal</strong></template>
Nachdem das Template geparst wurde, sieht das DOM so aus:
<div id="status">All systems <strong>normal</strong></div>
Die Chrome-Dokumentation erläutert, warum Verarbeitungsanweisungen statt Elementen gewählt wurden: Ein Patch über sie hinterlässt keine zusätzlichen Knoten. Dadurch funktioniert er auch dort, wo neue Elemente Probleme verursachen würden, etwa im <head> (zum Beispiel um den <title> zu ändern) oder innerhalb einer <table>.
Was sendet der Server?
Der Server schreibt sofort das komplette Gerüst, hält die Antwort offen, während er auf Daten wartet, und schreibt dann jedes <template for>, bevor er das Dokument schließt. Das folgende Beispiel verwendet ausschließlich node:http und ES-Module. Speichern Sie es als server.mjs und starten Sie es mit node server.mjs:
// server.mjs — run with: node server.mjs, then open http://localhost:3000
import { createServer } from 'node:http';
const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
const esc = (s) => String(s).replace(/[&<>"']/g, (c) =>
({ '&': '&', '<': '<', '>': '>', '"': '"', "'": ''' }[c]));
async function getStatus() { await sleep(1000); return 'All systems normal'; }
async function* getOrders() {
for (const o of ['Order #1001', 'Order #1002', 'Order #1003']) {
await sleep(700);
yield o;
}
}
createServer(async (req, res) => {
if (req.url !== '/') { res.writeHead(404).end(); return; }
res.writeHead(200, {
'Content-Type': 'text/html; charset=utf-8',
'Cache-Control': 'no-store',
});
// 1. Flush the shell immediately.
res.write(`<!doctype html>
<html><head><meta charset="utf-8"><title>Orders</title></head>
<body>
<h1>Your orders</h1>
<p id="status"><?start name="status">Checking status…<?end></p>
<ul id="orders"><?start name="orders"><li>Loading orders…</li><?end></ul>
`);
// 2. Templates are direct children of <body>, so they can reach the placeholders above.
const status = await getStatus();
res.write(`<template for="status">${esc(status)}</template>\n`);
// 3. Each template appends one item and leaves a fresh marker for the next.
for await (const order of getOrders()) {
res.write(`<template for="orders"><li>${esc(order)}</li><?marker name="orders"></template>\n`);
}
// 4. Close the document only after the last template.
res.end('</body></html>');
}).listen(3000);
Jeder Aufruf von response.write() sendet einen Chunk des Bodys, ohne auf end() zu warten. Hintergründe zur Verarbeitung von Chunked-Output finden Sie im Beitrag Web Streams Explained. Die sleep-Aufrufe simulieren langsame Abfragen und sind kein Benchmark.
Der Helfer esc() ist nötig, weil Template-Inhalt ganz normales HTML ist. Alle interpolierten Daten können Markup einschleusen, sofern Sie sie nicht escapen.
Wenn der Stream beendet ist, enthält #orders drei <li>-Elemente, gefolgt von einem abschließenden <?marker name="orders">. Öffnen Sie die Seite in Chrome 150 oder neuer, um zu sehen, wie sich die Liste füllt.
Pufferung hebelt Out-of-Order-Streaming aus
Out-of-Order-Streaming bringt nur dann etwas, wenn die Bytes den Browser nach und nach erreichen. Drei verbreitete Schichten halten die gesamte Antwort zurück: Kompressions-Middleware, die auf einen vollen Puffer wartet, Reverse Proxys oder CDNs mit aktivierter Response-Pufferung sowie Frameworks, die die gesamte Seite rendern, bevor sie irgendetwas senden. Prüfen Sie mit curl -N http://localhost:3000 über jede dieser Schichten hinweg, ob die Chunks einzeln ankommen. Das Flag -N deaktiviert die Ausgabepufferung von curl. Session Replay zeigt das Ergebnis aus Sicht der Nutzer, etwa einen leeren Bildschirm, der leer bleibt, bis die komplette Seite auf einmal eintrifft, weil ein Proxy den Stream zurückgehalten hat.
Anwendungsfälle und ihre Grenzen
Deklaratives Patching funktioniert nur, solange der Server noch das Dokument schreibt. Die Lebensdauer der Antwort ist also das Zeitfenster für Updates.
- Live-Liste. Endet jedes Template mit einem neuen
<?marker>desselben Namens, können spätere Templates an dieser Stelle weiter anhängen. Die Liste bleibt nur so lange live, wie die Verbindung offen ist. - Statuszeile. Ein einzelner
<?start>/<?end>-Bereich mit einem Template, wie im Beispiel oben. - Formularergebnis. Eine normale
POST-Navigation liefert eine Seite, deren Gerüst sofort gerendert wird und deren Ergebnis nachgestreamt wird, sobald die Verarbeitung abgeschlossen ist.
Alles, was nach dem Laden der Seite auf Klicks reagiert (Filter, Inline-Bearbeitung, Optimistic UI), benötigt weiterhin Client-State, JavaScript oder eine Bibliothek.
Deklarative Teilaktualisierungen vs. htmx
htmx sendet als Reaktion auf Nutzerereignisse neue HTTP-Requests und tauscht die zurückgegebenen Fragmente in die Seite ein. <template for> funktioniert nur innerhalb der bereits streamenden Antwort. Sobald der Server diese Antwort beendet, erfordern weitere Updates JavaScript oder eine neue Navigation.
htmx bleibt eine JavaScript-Bibliothek. htmx 4.0 führt ein <hx-partial>-Tag ein, mit dem sich mehrere Elemente aus einer Antwort ansteuern lassen. Muster wie Infinite Scroll mit htmx, bei dem ein Loader-Element beim Hineinscrollen in den sichtbaren Bereich htmx veranlasst, die nächste Seite vom Server abzurufen, und Formularvalidierung mit htmx beruhen auf Requests, die der Nutzer auslöst.
| htmx | <template for> | |
|---|---|---|
| Auslöser | Nutzerereignis | Schreibreihenfolge des Servers |
| Requests | Einer pro Interaktion | Eine Antwort |
| Ausführung in | JavaScript-Bibliothek | HTML-Parser |
| Ziele | Attribute mit CSS-Selektoren | Benannte Verarbeitungsanweisungen |
| Updates nach Ende der Antwort | Ja | Nein |
Wenn Sie doch JavaScript brauchen
Für Inhalte, die nach dem Laden eintreffen, ergänzt Chrome begleitende Methoden wie streamHTML und appendHTML samt Unsafe-Varianten und erweitert das bestehende setHTML. Entwickler können sie seit Chrome 148 hinter chrome://flags/#enable-experimental-web-platform-features testen; enthalten sind sie in Chrome 155, das am 16. September 2026 in die Beta-Phase ging.
const el = document.querySelector('#feed');
const response = await fetch('/feed-fragment.html');
await response.textStream().pipeTo(el.streamHTMLUnsafe());
response.textStream() ist ab Chrome 151 verfügbar. In älteren Versionen verwenden Sie stattdessen response.body.pipeThrough(new TextDecoderStream()).
Wenn Sie Markup durch streamHTMLUnsafe pipen, patcht jedes darin enthaltene <template for> die live angezeigte Seite. setHTML und innerHTML funktionieren anders: Sie parsen zunächst in ein separates Fragment, sodass Templates darin nur Knoten innerhalb dieses Fragments patchen können, nicht die bestehende Seite.
Browserunterstützung, Fallback und Einschränkungen
Out-of-Order-Streaming mit <template for> und den Verarbeitungsanweisungen <?marker>, <?start> und <?end> ist ab Chrome 150, das am 30. Juni 2026 als Stable-Version erschien, standardmäßig aktiviert. Das Feature wird auf ChromeStatus verfolgt, und das Attribut for ist Teil des WHATWG HTML Living Standard. Kein Browser außerhalb der Chromium-Familie hat es bisher ausgeliefert. Im WebKit-Standards-Positions-Thread schlug ein WebKit-Engineer vor, WebKits Position als „support“ festzulegen.
Der Intent to Ship bestätigt, dass Browser ohne Unterstützung die Verarbeitungsanweisungen als Kommentare parsen, sodass jeglicher Fallback-Inhalt sichtbar bleibt. Der Inhalt eines regulären <template>-Elements wird nicht gerendert, daher erscheinen die gestreamten Daten in diesen Browsern nie.
| Chrome 150+ | Keine Unterstützung | Mit template-for-polyfill | |
|---|---|---|---|
| Fallback-Text | Ersetzt | Bleibt sichtbar | Ersetzt |
| Gestreamter Inhalt | Eingepatcht | Im <template> verborgen | Eingepatcht, mit Parser-Einschränkungen |
Chrome veröffentlicht template-for-polyfill auf npm. Das Polyfill deckt die üblichen Fälle ab, hat aber keinen Zugriff auf den browsereigenen Parser, sodass einige Einschränkungen bleiben. Zudem ist es JavaScript – eine „No-JS“-Seite, die in jedem Browser funktionieren soll, hängt damit trotzdem von einem Skript ab. Das separate html-setters-polyfill übernimmt die Methodennamen der neuen APIs, sammelt jedoch zunächst den gesamten Inhalt und fügt ihn am Ende ein – es streamt also nicht.
Einschränkungen von <template for>, die Sie kennen sollten:
- Ein Template kann nur Verarbeitungsanweisungen erreichen, die dasselbe Elternelement haben. Platzieren Sie Templates direkt unter
<body>, erreichen sie das gesamte Dokument einschließlich<head>. - Sie können
<?end>weglassen. In diesem Fall reicht die Ersetzung von<?start>bis zum Ende des Elternelements. - Verschieben Sie eine Verarbeitungsanweisung, während noch ein Template hineinstreamt, kann der restliche Inhalt weiterhin an der alten Stelle landen.
Fazit
Out-of-Order-Streaming verlagert die Routine aus Abrufen, Finden und Austauschen in den HTML-Parser – allerdings nur innerhalb einer einzigen offenen Antwort. Wo immer Sie es einsetzen, platzieren Sie die eigentlichen Daten in <?start>/<?end>-Fallback-Bereichen, damit Browser ohne Unterstützung trotzdem etwas Sinnvolles anzeigen. Betreiben Sie anschließend server.mjs hinter Ihrem tatsächlichen Proxy und CDN, um zu prüfen, ob die Chunks einzeln ankommen, bevor Sie sich in der Produktion darauf verlassen.
FAQs
Worin unterscheidet sich template for vom Streaming mit React Suspense?
Beim Streaming-SSR von React wird jede aufgelöste Suspense-Boundary als verstecktes div gesendet, gefolgt von einem Inline-Skript, das es gegen den Fallback austauscht. Sind Skripte deaktiviert oder blockiert eine Content Security Policy Inline-Skripte ohne Nonce, gelangt dieser Inhalt nie an seinen Platz. Bei template for übernimmt der HTML-Parser den Austausch selbst, sodass kein Skript ausgeführt wird und die Seite keine Freigabe für Inline-Skripte benötigt.
Gibt es einen browserübergreifenden Weg, HTML ohne JavaScript out of order zu streamen?
Ja. Declarative Shadow DOM mit benannten Slots funktioniert in Chrome, Firefox und Safari. Das Gerüst ist ein Template mit shadowrootmode='open', das slot-Elemente mit Fallback-Inhalt enthält. Elemente mit passendem slot-Attribut werden später nachgestreamt und füllen diese Slots. Der Nachteil: Der Shadow Root sitzt in der Regel auf body, sodass CSS auf Dokumentebene das Gerüst-Markup innerhalb des Shadow Trees nicht stylt.
Was passiert, wenn eine langsame Abfrage fehlschlägt, nachdem das Seitengerüst bereits gesendet wurde?
HTTP-Status und Header stehen fest, sobald der erste Chunk geschrieben ist. Der Server kann also nicht mehr auf einen 500-Fehler, eine Weiterleitung oder neue Cookies umschwenken. Behandeln Sie den Fehler stattdessen im Markup: Schreiben Sie ein Template mit demselben Namen, das eine Fehlermeldung enthält – es ersetzt den Lade-Fallback. In Node löst ein Aufruf von res.setHeader nach dem Senden der Header den Fehler ERR_HTTP_HEADERS_SENT aus.
Complete picture for complete understanding
Capture every clue your frontend is leaving so you can instantly get to the root cause of any issue with OpenReplay — the open-source session replay tool for developers. Self-host it in minutes, and have complete control over your customer data.
Star on GitHub12k