Out-of-Order (OOO) HTML Streaming

Out-of-Order-HTML-Streaming (auch als deklarative Teilaktualisierungen bezeichnet) ist eine Alternative zur herkömmlichen linearen Verarbeitung von HTML von oben nach unten. Damit können Entwickler Platzhalter früher in einem Dokument definieren und sie mit Inhalten „patchen“, die später im selben Netzwerkstream oder über neue imperative JavaScript-Streaming-APIs eingehen.

Kurzer Überblick

Die Funktion besteht aus zwei Hauptmechanismen:

  1. Deklaratives Patching (HTML-basiert): Verwendet neue Verarbeitungsanweisungen (<?marker>, <?start>, <?end>) und <template for>, um Inhalte im DOM auszutauschen, während sie vom Server gestreamt werden.
  2. Imperative Einstellung und Streaming (JS-basiert): Eine Reihe neuer Methoden (z.B. streamHTML(), setHTMLUnsafe()), mit denen HTML festgelegt oder lesbare Streams direkt in DOM-Elemente geleitet werden können, mit nativer Leistung und optionaler Skriptausführung.

Best Practices

  • Inhalte ohne Scrollen priorisieren: Streamen Sie zuerst wichtige UI-Elemente (Header, Hero-Image, primäre Navigation, primäre Inhalte). Verwenden Sie Platzhalter für „teure“ oder weniger wichtige Komponenten wie Kommentare, Anzeigen oder ähnliche Inhalte.
  • Native Ladestatus implementieren: Verwenden Sie die Markierungen <?start> und <?end>, um Fallback-Content einzuschließen. Der Browser ersetzt automatisch alles zwischen diesen Markierungen, wenn die entsprechende <template for> eintrifft.
  • Cumulative Layout Shift (CLS) minimieren: Geben Sie immer stabile Abmessungen für Platzhalter an. Wenn die endgültige Inhaltsgröße unbekannt ist, verwenden Sie Skeleton Screens oder CSS-aspect-ratio, um Platz zu reservieren und störende Verschiebungen zu vermeiden.
  • Bedienungshilfen und Fokusverwaltung: Beachten Sie, dass Aktualisierungen in falscher Reihenfolge Nutzer von Screenreadern verwirren können. Verwenden Sie aria-live-Regionen für wichtige Updates und achten Sie darauf, dass der Fokus nicht verloren geht, wenn ein Platzhalter mit dem aktiven Element ersetzt wird.

Details zur Implementierung

1. Deklarative HTML-Markierungen

Der Browser sucht nach bestimmten Verarbeitungsanweisungen, um zu ermitteln, wo Inhalte gepatcht werden sollen.

<!-- A simple insertion point -->
<div id="sidebar">
  <?marker name="dynamic-comments">
</div>

<!-- A replacement range with loading state -->
<main>
  <?start name="main-content">
    <div class="skeleton">Loading content...</div>
  <?end>
</main>

... (other content) ...

<!-- Later in the same stream, provide the content -->
<template for="dynamic-comments">
  <div class="comments">...</div>
</template>

<template for="main-content">
  <article>
    <h1>The Real Content</h1>
    <p>This replaces the skeleton div automatically.</p>
  </article>
</template>

Sobald ein <template> einen Einfügepunkt ersetzt, ist dieser Einfügepunkt nicht mehr vorhanden und kann nicht wiederverwendet werden. Ein <template> kann jedoch Einfügemarken enthalten, die später im Stream verwendet werden können.

 <?start name="replaced-content">
  <div class="skeleton">
<?end>
<!-- Later in the same stream, provide initial content -->
<template for="replaced-content">
  <?start name="replaced-content">
    <div>Quickly computed result</div>
  <?end>
</template>
<!-- Later in the same stream, provide full content -->
<template for="replaced-content">
  <?start name="replaced-content">
    <div>Full result</div>
  <?end>
</template>

2. Imperatives JS-Streaming

Der Browser bietet eine Reihe von 24 Methoden (6 Aktionen mit jeweils 4 Variationen), um das DOM entweder statisch (aus einem String) oder über einen Stream zu aktualisieren.

API-Referenzmatrix

Aktion Statisch (sicher) Streaming (Safe) Statisch (unsicher) Streaming (unsicher)
Set setHTML() streamHTML() setHTMLUnsafe() streamHTMLUnsafe()
Ersetzen replaceWithHTML() streamReplaceWithHTML() replaceWithHTMLUnsafe() streamReplaceWithHTMLUnsafe()
Vorher beforeHTML() streamBeforeHTML() beforeHTMLUnsafe() streamBeforeHTMLUnsafe()
Voranstellen prependHTML() streamPrependHTML() prependHTMLUnsafe() streamPrependHTMLUnsafe()
Anhängen appendHTML() streamAppendHTML() appendHTMLUnsafe() streamAppendHTMLUnsafe()
Nachher afterHTML() streamAfterHTML() afterHTMLUnsafe() streamAfterHTMLUnsafe()

Schlüsselkonzepte

  • Sicher oder unsicher?:
    • Sichere Methoden (z.B. setHTML()) verwenden standardmäßig einen integrierten Sanitizer, um potenziell gefährliche Inhalte wie <script>-Tags zu entfernen. Sie können ein benutzerdefiniertes Sanitizer-Objekt in den Optionen übergeben, das zusätzlich zum Standard-Sanitizer angewendet werden soll.
    • Unsichere Methoden (z.B. setHTMLUnsafe()) bereinigen standardmäßig nicht, aber Sie können trotzdem einen benutzerdefinierten Sanitizer übergeben. Damit Skripts im neuen Inhalt ausgeführt werden können, müssen Sie { runScripts: true } explizit in den Optionen übergeben. WICHTIG: Verwenden Sie unsichere Methoden nur für vertrauenswürdige Inhalte, wenn sichere Methoden nicht funktionieren würden.
  • Statisch im Vergleich zu Streaming:
    • Statische Methoden nehmen einen String (oder TrustedHTML) und wenden ihn sofort an.
    • Streamingmethoden geben einen WritableStream zurück. So können Sie Inhalte (z. B. aus einer fetch-Antwort) direkt in das DOM leiten. Der Browser rendert sie dann schrittweise, sobald die Chunks eintreffen.

Beispiel: Fetch-Antwort streamen

Sie können Fetch-Antworten direkt in das DOM leiten, ohne dass eine manuelle Chunk-Verarbeitung erforderlich ist.

const main = document.querySelector('main');
const response = await fetch('/api/partial-update');

// Pipe the stream directly into the element
await response.body
  .pipeThrough(new TextDecoderStream())
  .pipeTo(main.streamHTMLUnsafe({ runScripts: true }));

// Or use the `textStream()` convenience method which streams directly without needing the intermediate `TextDecoderStream()` step
await response
  .textStream()
  .pipeTo(main.streamHTMLUnsafe({ runScripts: true }))

Referenzmatrix für Anwendungsfälle

  • Inselarchitektur: Unabhängige interaktive Komponenten werden über statischem HTML ohne vollständige Framework-Laufzeit hydriert.
  • Verzögerte „aufwendige“ Blöcke: Das Grundgerüst einer Seite wird sofort bereitgestellt und datenbankintensive Komponenten werden gestreamt, sobald sie verfügbar sind.
  • Updates für Single-Page-Anwendungen (SPA): Streamen Sie große UI-Übergänge in SPAs mit streamHTML(), anstatt auf eine vollständige JSON-Nutzlast und die manuelle DOM-Konstruktion zu warten.

Anti-Patterns und Warnungen (NICHT EMPFOHLEN)

  • Verwenden Sie OOO-Streaming nicht für alles. Eine zu starke Fragmentierung einer Seite kann die Komplexität des Streams erhöhen und möglicherweise die Bereitstellung von Inhalten verzögern, wenn der Browser zu viel Zeit mit dem Patchen kleiner Knoten verbringt.
  • Ignorieren Sie Trusted Types nicht. Wenn Sie *Unsafe-Methoden in Umgebungen verwenden, in denen Trusted Types erzwungen werden, müssen Sie bei Bedarf ein bereinigtes Richtlinienobjekt übergeben.
  • Gehen Sie nicht von einer bestimmten Ausführungsreihenfolge der Skripts aus. Skripts in einem <template for> werden ausgeführt, sobald sie in das Dokument eingefügt werden. Verlassen Sie sich nicht darauf, dass sie in einer bestimmten Reihenfolge im Verhältnis zu anderen verzögerten Vorlagen ausgeführt werden.
  • Nicht ohne Polyfill-Strategie verwenden. Sorgen Sie für ein Fallback oder verwenden Sie Polyfills (html-setters-polyfill und template-for-polyfill), um die erforderlichen Browser zu unterstützen, bis diese Funktion Baseline wird.

Fallback-Strategien

Sie können Polyfills wie template-for-polyfill und html-setters-polyfill verwenden, um die API-Oberfläche für nicht unterstützte Browser zu emulieren.

Browser ohne html-processing-instructions-Unterstützung parsen <?marker>, <?start> und <?end> als Kommentare. template-for-polyfill erkennt diese Kommentare weiterhin als Markierungen. Verwenden Sie daher für alle Browser dieselbe Syntax für Verarbeitungsanweisungen. Schreiben Sie die Markierungen nicht als HTML-Kommentare oder -Elemente für nicht unterstützte Browser um.