声明式部分更新

发布时间:2026 年 5 月 19 日

网络早已不再是最初的静态、以文档为导向的媒体。现代富 Web 应用被所有人广泛使用,原因有很多,从通信、购买、消费富媒体内容到管理我们复杂的生活。

尽管 HTML 取得了长足的进步,但仍以从上到下的顺序交付,很少考虑内容何时准备就绪或用户何时使用。借助 CSS,您可以更改内容的顺序,但通常会产生严重的无障碍副作用。JavaScript 可让您通过各种 API 来操作 DOM,从而摆脱这种限制,但这些 API 通常需要冗长的语法或构建 DOM 树才能插入到 HTML 中。

鉴于 Web 的客户端-服务器特性,性能对于 Web 来说非常重要,但人们往往会做出次优选择来规避 HTML 的这种顺序特性,从而降低性能。这包括等待整个页面准备就绪,或使用大型框架以异步方式交付组件。JavaScript 框架的普及表明,Web 开发者更喜欢基于组件的模型,而不是 Web 最初的僵化文档心理模型。

Chrome 团队一直在考虑这个问题,并一直在开发名为声明式部分更新的 Web 平台新功能。

借助两组新的 API,您可以更轻松地以非线性方式交付 HTML,无论是 HTML 文档本身中的无序交付,还是通过使用新的 JavaScript API 以更简单的方式将 HTML 动态插入到现有文档中。这些功能已准备就绪,开发者可以从 Chrome 148 开始使用 chrome://flags/#enable-experimental-web-platform-features 标志进行测试。您还可以使用 Polyfill 立即使用这些新 API,即使在尚不支持这些 API 的浏览器中也是如此。

这些对 Web 平台的补充功能正在标准化,并获得了其他浏览器供应商和标准化渠道的积极反馈。我们正在更新相关标准,以纳入这些新 API。

无序流式传输

第一组更改是新的无序流式传输 API,它们使用 <template> HTML 元素和处理指令占位符。例如:

<div>
  <?marker name="placeholder">
</div>

...

<template for="placeholder">
  Here is some <em>HTML content</em>!
</template>

处理指令在 XML 中存在已久,但在 HTML 中一直被视为注释并被忽略。这一新 API 改变了这种情况,并为 HTML 带来了处理指令。当浏览器看到 <?marker name="placeholder"> 处理指令时,不会立即执行任何操作(与之前一样),但可以在稍后引用这些指令。

<template> 元素会通过 name 属性查找相应的处理指令,并替换内容。在这种情况下,解析后的 DOM 最终如下所示:

<div>
  Here is some <em>HTML content</em>!
</div>

除了用于替换的 <?marker> 属性之外,还有 <?start><?end> 范围标记,用于在处理模板之前显示临时占位内容:

<div>
  <?start name="another-placeholder">
  Loading…
  <?end>
</div>

...

<template for="another-placeholder">
  Here is some <em>HTML content</em>!
</template>

在这种情况下,系统会显示 Loading…,直到用户看到 <template>,然后将其替换为新内容。

您还可以在模板中添加处理指令,以实现多次更新:

<ul id="results">
  <?start name="results">
  Loading…
  <?end>
</ul>

...

<template for="results">
  <li>Result One</li>
  <?marker name="results">
</template>
...

<template for="results">
  <li>Result Two</li>
  <?marker name="results">
</template>
...

解析后会生成以下 HTML:

<ul id="results">
  <li>Result One</li>
  <li>Result Two</li>
  <?marker name="results">
</ul>

最后一条处理指令位于末尾,以防日后向文档中添加更多 <template for="results">

演示

在此视频中,我们使用流式 HTML 实现了一个基本的相册应用:

使用无序流式传输实现的相册演示(来源

初始布局完成后,状态和照片都会流式传输到 HTML 中。

使用场景

当与流式 HTML 结合使用时,这种无序修补 HTML 的方法可应用于多种场景:

  • 孤岛架构。一种常见的设计模式,由 Astro 等框架推广开来,即岛屿架构,其中组件在静态 HTML 的基础上独立进行水合。借助 <template for> API,可以直接在 HTML 中以类似方式处理静态内容。JavaScript 框架还可以使用此功能来创建更具互动性的孤岛或处理组件。
  • 在内容准备就绪时交付内容。借助这种孤岛架构,内容可以在准备就绪时进行流式传输,而不会因需要额外处理的内容(例如数据库查找)而延迟。虽然许多平台都允许流式传输 HTML,但 HTML 的有序性意味着内容通常会被延迟,或者需要借助复杂的 JavaScript DOM 操作才能实现。现在,您可以在等待时提供静态内容,然后在 HTML 流的末尾插入更昂贵的内容。
  • HTML 可以按最佳顺序传送,以提高网页加载性能。更进一步说,即使订单已准备就绪,您也可以更改订单。例如,超级菜单是一种常见的导航功能,其中包含大量 HTML,用户只有在网页变为可互动状态时才会看到这些 HTML。此大块 HTML 可在 HTML 文档中稍后提供,以便优先处理初始网页加载所需的更重要 HTML。HTML 不再受顺序的限制。

以上仅为部分使用情形,我们非常期待看到开发者如何使用这一新 API。

限制和细微差别

该 API 包含一些需要注意的限制和细微之处:

  • 出于安全考虑,<template for> 只能更新同一父元素内的处理指令。直接向 <body> 元素添加 <template for> 可使其访问整个文档(包括 <head>)。
  • <?end> 处理指令是可选的,如果缺少该指令,系统会替换 <?start> 元素与包含元素末尾之间的内容。
  • <template for> 开始流式传输后移动处理指令也可能会产生意想不到的后果,新内容会继续流式传输到旧位置。
  • 请注意,如果使用 setHTMLinnerHTML 等方法动态插入 <template for>,则在解析时,模板的“父级”是一个中间文档片段。这意味着,使用这些方法插入 HTML 无法修改现有 DOM,并且修补会在 fragment 内“就地”进行。不过,如果使用 streamHTMLUnsafe 等方法进行流式传输(我们即将介绍),则没有中间 fragment,因此模板可以替换现有内容。

未来可能新增的功能

我们正在考虑添加一些潜在的未来功能,包括:

  • 客户端包含项。例如 <template for="footer" patchsrc="/partials/footer.html">
  • 批处理。在客户端,还可以扩展 fragment include 以处理批处理,从而确保同时进行多次更新。
  • 防止覆盖不会更改的内容。这可以通过内容修订号或版本控制来实现。这样一来,状态就可以在路线更改或其他更新之间保持不变,而不是重置内容。
  • 在修补时进行清理。例如 <template for=icon safe><svg id="from-untrusted-source">...</svg></template>

Polyfill

Chrome 团队已发布 template-for-polyfill,该软件包可在 npm 上获取,以便网站立即使用这项新功能,即使其他浏览器尚未支持该功能。

由于它无法直接更新浏览器的 HTML 解析器,因此存在一些限制,但它涵盖了最常见的用例。网站仍应在其他浏览器中进行测试。

更新了 HTML 插播和流式处理方法

并非所有内容都可以 HTML 格式传送。Chrome 在这方面所做的另一项工作是让用户能够更轻松地通过 JavaScript 更新内容。

目前已有多种方法可使用 JavaScript 将 HTML 动态注入现有文档中:

  • setHTML
  • setHTMLUnsafe
  • innerHTMLouterHTML 设置器
  • createContextualFragment
  • insertAdjacentHTML

不过,它们的工作方式略有不同,存在一些细微差别,开发者可能并不总是会考虑到这些差别:

  • 新内容是覆盖还是附加?
  • 它们是否会通过转义 <script> 标记等方式来清理可能存在危险的 HTML?
  • 如果没有,是否应运行 <script>
  • 它们如何与 TrustedTypes 搭配使用?

很少有开发者能够诚实地查看这些 API,并自信地回答有关每个 API 的这些问题。

一个很大的限制是,它们只能用于预先知道的完整 HTML 集,而之前曾有调用允许流式传输 HTML。实际上,这意味着您需要先下载整个内容,然后才能插入该内容,而 HTML 的一个优势是能够立即串流内容。虽然可以通过拆分载荷或使用 document.write 等过时的黑客方法来有限地解决此问题,但这些方法会带来自己的问题。

一组新的静态和流式传输 API

Chrome 提出了一套新 API 和对现有 setHTMLsetHTMLUnsafe 的扩展,以清理此问题,并引入流式传输功能:

您可以使用相应方法来设置或替换内容,也可以使用相应方法在现有 HTML 之前或之后插入内容。每种方法都有相应的流式方法:

操作 静态 流式
设置元素的内容(采用 HTML 格式) setHTML(html, options); streamHTML(options);
将整个元素替换为以下 HTML replaceWithHTML(html, options); streamReplaceWithHTML(options);
在元素之前添加 HTML beforeHTML(html, options); streamBeforeHTML(options);
将 HTML 添加为元素的第一个子级 prependHTML(html, options); streamPrependHTML(options);
将 HTML 添加为元素的最后一个子级 appendHTML(html, options); streamAppendHTML(options);
在元素后添加 HTML afterHTML(html, options); streamAfterHTML(options);
新的插入和流式传输方法

我们稍后还会介绍 Unsafe 版本。虽然这些方法看起来很多(尤其是当您添加 Unsafe 等效方法时),但一致的命名惯例使得每种方法的用途比之前提到的不相关方法更清晰。

静态版本将新的 HTML 作为 DOM 字符串实参,并提供可选的选项:

const newHTML = "<p>This is a new paragraph</p>";
const contentElement = document.querySelector('#content-to-update');

contentElement.setHTML(newHTML);

流式传输版本可与 Streams API 搭配使用,例如与 getWriter() 搭配使用:

const contentElement = document.querySelector('#content-to-update');
const writer = contentElement.streamHTMLUnsafe().getWriter();

// Example stream of updating content
while (true) {
 await writer.write(`<p>${++i}</p>`);
 await new Promise((resolve) => setTimeout(resolve, 1000));
}

writer.close();

或者,通过 pipe chains 从提取响应中获取:

const contentElement = document.querySelector('#content-to-update');

const response = await fetch('/api/content.html');

response.body
  .pipeThrough(new TextDecoderStream())
  .pipeTo(contentElement.streamHTMLUnsafe());

我们还计划添加一种便捷方法,让您可以直接进行流式传输,而无需执行中间的 TextDecoderStream() 步骤。

借助 options 实参,您可以指定自定义 sanitizer,该实参的默认值为 default,表示默认清理器配置。其使用方式如下:

const newHTML = "<p>This is a new paragraph</p>";
const contentElement = document.querySelector('#content-to-update');

// Only allows basic formatting
const basicFormattingSanitzer = new Sanitizer({ elements: ["em", "i", "b", "strong"] });

contentElement.setHTML(newHTML, {sanitizer: basicFormattingSanitzer});

“不安全”的方法

每个 API 还有“不安全”版本:

操作 静态 流式
设置元素的内容(采用 HTML 格式) setHTMLUnsafe(html,options); streamHTMLUnsafe(options);
将整个元素替换为以下 HTML replaceWithHTMLUnsafe(html, options); streamReplaceWithHTMLUnsafe(options);
在元素之前添加 HTML beforeHTMLUnsafe(html, options); streamBeforeHTMLUnsafe(options);
将 HTML 添加为元素的第一个子级 prependHTMLUnsafe(html, options); streamPrependHTMLUnsafe(options);
将 HTML 添加为元素的最后一个子级 appendHTMLUnsafe(html, options); streamAppendHTMLUnsafe(options);
在元素后添加 HTML afterHTMLUnsafe(html, options); streamAfterHTMLUnsafe(options);
“不安全”的插入和流式传输方法

这些“不安全”的方法默认会关闭清理器(您可以根据需要指定自定义清理器),并且还允许使用可选的 runScripts 选项(默认为 false)运行脚本。

setHTML 类似,setHTMLUnsafe 也是一种现有方法,但已向其中添加 runScripts 选项参数,以便将其用于脚本执行:

const newHTML = `<p>This is a new paragraph</p>
                 <script src=script.js></script>`;
const contentElement = document.querySelector('#content-to-update');

contentElement.setHTMLUnsafe(newHTML, {runScripts: true});

方法中的“不安全”字样是为了提醒开发者潜在的风险以及他们可能需要如何清理或限制脚本,而不是说不应使用这些方法。

这种做法有多“不安全”取决于输入的信任程度。所有 Unsafe 静态方法都可使用 DOM 字符串或 TrustedHTML 作为 html 实参,并且还允许使用清理器。不过,使用 runScript 的全部意图是允许脚本,因此默认情况下不使用清理器。

使用场景

借助这些新 API,开发者可以更轻松地向现有网页添加 HTML,并添加具有一致名称和选项的新 API。流式 API 具有性能优势,无需等到所有新内容都可供平台使用。

用例包括:

  • 在单页应用中动态流式传输大型内容更新。如前所述,当前 SPA 的一个主要缺点是无法受益于初始 HTML 加载的流式传输特性,但现在情况不同了!
  • 插入 HTML 页脚等常见内容。使用 JavaScript API 可让您提取部分内容并将其插入网页中,从而受益于缓存,而不是在发送的每个网页中重复这些内容。不过,由于需要依赖 JavaScript 才能运行,因此这种方法仅适用于在初始加载时不会显示的内容。

同样,以上只是几个示例,我们非常期待看到大家的创意!

限制和细微差别

这些新 API 还包含一些需要注意的限制和细微差别:

  • 将流式传输与 Trusted Types API 集成需要使用新的 createParserOptions 方法,该方法允许将清理器注入到任何 HTML 设置操作中。如需详细了解可信类型集成,请参阅相关说明
  • <template for> 类似,将元素移动到正在流式传输的元素中可能会产生意外后果或导致流式传输错误。
  • streamHTMLUnsafe 在许多方面都更像主解析器,包括在将 <template for> 指令添加到主文档时处理这些指令,以及将 defer 脚本延迟到流结束时处理。

Polyfill

Chrome 团队已发布 html-setters-polyfill,该软件包可在 npm 上获取,以便网站立即使用这项新功能,即使其他浏览器尚未支持该功能。

请注意,此 polyfill 不会进行流式传输,而是在完成后进行缓冲和应用。它更像是 API 形态的填充区,而不是功能填充区。

此外,设置安全内容取决于 setHTMLSanitizer API,而 Safari 不支持这两者。

同时使用这两者

虽然这两个 API 是分开的,但将它们结合起来才能真正发挥作用。通过将新的 <template for> 元素流式传输到 HTML 中,您可以动态更新内容的不同部分,而无需使用单独的 JavaScript 引用直接定位到 DOM 中的每个部分。

基本的 SPA 样式网页加载可以通过以下方式实现:加载包含处理指令的轮廓网页,然后将每个新网页的模板流式传输到 HTML 的底部,以插入到这些处理指令中。

毫无疑问,这两种 API 还有更多潜力和用例,因此不要让我们的(有限的!)想象力阻碍您。通过更轻松地管理部分更新,您可以减少一些样板代码,更轻松地进行更新,并释放 Web 的新潜力!