乱序 (OOO) HTML 流式传输

乱序 (OOO) HTML 流式传输(也称为声明式部分更新)是传统“自上而下”线性 HTML 处理方式的替代方案。它允许开发者在文档中较早地定义占位符,并使用在同一网络流中较晚到达的内容或通过新的命令式 JavaScript 流式传输 API 来“修补”这些占位符。

简要概览

该功能包含两个主要机制:

  1. 声明式修补(基于 HTML):使用新的处理指令(<?marker>、<?start>、<?end>)和 <template for>,在内容从服务器流式传输进来时,在 DOM 中交换内容。
  2. 命令式设置和流式传输(基于 JS):一套新方法(例如 streamHTML()、setHTMLUnsafe()),可用于设置 HTML 或将可读流直接通过管道传输到 DOM 元素,并具有原生性能和可选脚本执行功能。

最佳做法

  • 优先显示首屏内容:首先流式传输关键界面(标题、主打内容、主要导航、主要内容)。为“昂贵”或优先级较低的组件(例如评论、广告或相关内容)使用占位符。
  • 实现原生加载状态:使用 <?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 种变体)的 API,用于以静态方式(通过字符串)或通过流来更新 DOM。

API 参考文档矩阵

操作 静态(安全) 流式传输(安全) 静态(不安全) 流式传输(不安全)
集 setHTML() streamHTML() setHTMLUnsafe() streamHTMLUnsafe()
替换 replaceWithHTML() streamReplaceWithHTML() replaceWithHTMLUnsafe() streamReplaceWithHTMLUnsafe()
之前 beforeHTML() streamBeforeHTML() beforeHTMLUnsafe() streamBeforeHTMLUnsafe()
前置 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 中流式传输大型界面转换,而无需等待完整的 JSON 载荷和手动 DOM 构建。

反模式和警告(请勿这样做)

  • 不要对所有内容都使用 OOO 流式传输。如果页面过度分片,可能会增加数据流的复杂性,并且如果浏览器花费过多时间修补小节点,可能会导致内容延迟。
  • 请勿忽略可信类型。在强制执行可信类型的环境中使用 *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 注释或元素,以供不支持的浏览器使用。