乱序 (OOO) HTML 流式传输(也称为声明式部分更新)是传统“自上而下”线性 HTML 处理方式的替代方案。它允许开发者在文档中较早地定义占位符,并使用在同一网络流中较晚到达的内容或通过新的命令式 JavaScript 流式传输 API 来“修补”这些占位符。
简要概览
该功能包含两个主要机制:
- 声明式修补(基于 HTML):使用新的处理指令(
<?marker>、<?start>、<?end>)和<template for>,在内容从服务器流式传输进来时,在 DOM 中交换内容。 - 命令式设置和流式传输(基于 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 注释或元素,以供不支持的浏览器使用。