不按順序 (OOO) HTML 串流

不按順序 (OOO) HTML 串流 (也稱為宣告式部分更新) 是傳統「由上而下」線性處理 HTML 的替代方案。開發人員可以先在文件中定義預留位置,然後使用相同網路串流中稍後抵達的內容,或透過新的 JavaScript 命令式串流 API「修補」這些預留位置。

快速總覽

這項功能包含兩項主要機制:

  1. 宣告式修補 (HTML 型):使用新的處理指令 (<?marker>、<?start>、<?end>) 和 <template for>,在 DOM 從伺服器串流傳入時,替換 DOM 中的內容。
  2. 指令式設定和串流 (以 JS 為基礎):一系列新方法 (例如 streamHTML()、setHTMLUnsafe()),可設定 HTML 或將可讀取的串流直接傳送至 DOM 元素,並提供原生效能和選用指令碼執行功能。

最佳做法

  • 優先處理不需捲動位置的內容:優先串流重要 UI (標頭、主頁橫幅、主要導覽、主要內容)。針對「昂貴」或優先順序較低的元件 (例如留言、廣告或相關內容),請使用預留位置。
  • 導入原生載入狀態:使用 <?start> 和 <?end> 標記包裝備用內容。當對應的 <template for> 抵達時,瀏覽器會自動取代這些標記之間的所有內容。
  • 盡量降低 Cumulative Layout Shift (CLS):一律為預留位置提供穩定的尺寸。如果無法預測最終內容大小,請使用骨架畫面或 CSS aspect-ratio 預留空間,避免畫面劇烈跳動。
  • 無障礙功能和焦點管理:請注意,不按順序更新可能會造成螢幕閱讀器使用者困惑。請使用 aria-live 區域提供重要更新,並確保含有活動元素的預留位置遭到取代時,焦點不會消失。

作品詳細資訊

1. 宣告式 HTML 標記

瀏覽器會尋找特定處理指令,找出應修補內容的位置。

<!-- 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>

<template> 取代插入點後,該插入點就不會再存在,也無法重複使用。不過,<template> 可能包含插入點,可在串流中稍後使用。

 <?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. 命令式 JS 串流

瀏覽器提供 24 種方法 (6 種動作,每種動作有 4 種變化版本),可透過靜態 (從字串) 或串流方式更新 DOM。

API 參考資料矩陣

動作 靜態 (安全) 串流 (安全) 靜態 (不安全) 串流 (不安全)
組合 setHTML() streamHTML() setHTMLUnsafe() streamHTMLUnsafe()
取代 replaceWithHTML() streamReplaceWithHTML() replaceWithHTMLUnsafe() streamReplaceWithHTMLUnsafe()
變更前 beforeHTML() streamBeforeHTML() beforeHTMLUnsafe() streamBeforeHTMLUnsafe()
Prepend prependHTML() streamPrependHTML() prependHTMLUnsafe() streamPrependHTMLUnsafe()
附加 appendHTML() streamAppendHTML() appendHTMLUnsafe() streamAppendHTMLUnsafe()
變更後 afterHTML() streamAfterHTML() afterHTMLUnsafe() streamAfterHTMLUnsafe()

核心概念

  • 安全與不安全:
    • 安全方法 (例如 setHTML()) 預設會使用內建的淨化工具,移除 <script> 標記等潛在危險內容。您可以在選項中傳遞自訂 Sanitizer 物件,以便在預設清理工具之外套用。
    • 不安全方法 (例如 setHTMLUnsafe()) 預設不會清理,但您仍可傳遞自訂 Sanitizer。如要允許指令碼在新內容中執行,您必須在選項中明確傳遞 { runScripts: true }。重要事項:只有在安全方法無法運作時,才對信任的內容使用不安全的方法。
  • 靜態與串流:
    • 靜態方法會採用字串 (或 TrustedHTML) 並立即套用。
    • 串流方法會傳回 WritableStream。這樣一來,您就能將內容 (例如 fetch 回應) 直接傳送至 DOM,瀏覽器則會在區塊送達時逐步算繪。

範例:串流擷取回應

您可以直接將擷取回應傳送至 DOM,不必手動處理區塊。

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 }))

用途參考矩陣

  • 孤島架構:在靜態 HTML 上,為獨立的互動式元件進行凍結,不需完整的架構執行階段。
  • 延遲載入「耗費資源」的區塊:立即提供網頁外殼,並在資料庫密集型元件可用時串流載入。
  • 單頁應用程式 (SPA) 更新:使用 streamHTML() 串流 SPA 中的大型 UI 轉換,不必等待完整的 JSON 酬載和手動 DOM 建構。

反面模式和警告 (請勿這樣做)

  • 不要將 OOO 串流用於所有內容。如果網頁過度分割,可能會增加串流的複雜度,且瀏覽器花費太多時間修補小型節點,可能會延遲內容。
  • 請勿忽略 Trusted Types。在強制執行 Trusted Types 的環境中使用 *Unsafe 方法時,請務必視需要傳遞經過清理的政策物件。
  • 請勿假設指令碼執行順序。<template for> 內的指令碼在修補到文件後會立即執行。請勿依賴這些範本相對於其他延遲範本的特定執行順序。
  • 請勿在沒有 Polyfill 策略的情況下使用。請務必提供備用方案,或使用 Polyfill (html-setters-polyfill 和 template-for-polyfill) 支援所需瀏覽器,直到這項功能成為基準。

備用策略

您可以使用 template-for-polyfill 和 html-setters-polyfill 等 polyfill,為不支援的瀏覽器模擬 API 介面。

不支援 html-processing-instructions 的瀏覽器會將 <?marker>、<?start> 和 <?end> 剖析為註解。template-for-polyfill 仍會將這些註解視為標記,因此請為所有瀏覽器保留相同的處理指令語法。請勿將標記重寫為 HTML 註解或不支援瀏覽器的元素。