预渲染自定义元素

Web 功能: 声明式 shadow DOM

预渲染是指从服务器端发送自定义元素的标记(包括其 shadow tree),以便在运行任何 JavaScript 之前,组件就已完成样式设置和结构化。为此,主要平台基元是声明式 Shadow DOM (DSD):解析器仅从 HTML 附加 shadow root,随后元素会通过其类(如果已注册)进行升级(“水合”)。此模式适用于服务器端渲染、流式传输,以及避免出现无样式内容闪烁 (FOUC)。

编写声明式 shadow root

将具有 shadowrootmode 属性的 <template> 嵌套为主机的第一个子项。解析器会将其内容附加为 shadow root 并移除模板,无需脚本。

<my-card>
  <template shadowrootmode="open">
    <style>
      .card { border: 1px solid; border-radius: 8px; padding: 1rem; }
    </style>
    <div class="card">
      <slot name="header"></slot>
      <div class="body"><slot></slot></div>
    </div>
  </template>

  <!-- Light DOM children are projected into the slots above. -->
  <h2 slot="header">Server-rendered title</h2>
  <p>Visible and styled before JS loads.</p>
</my-card>

补水时机

注册匹配的类后,浏览器会升级宿主。由于影子根已经存在,因此构造函数不得盲目创建第二个影子根:

class MyCard extends HTMLElement {
  constructor() {
    super();
    // Reuse the server-rendered shadow root if present; only create one if absent.
    if (!this.shadowRoot) {
      this.attachShadow({ mode: 'open' });
      this.shadowRoot.append(template.content.cloneNode(true));
    }
    // Hydrate in place: attach listeners once against existing markup (not unguarded
    // in connectedCallback, which re-runs if the element moves in the DOM), and do
    // NOT re-render; that would discard the server's DOM and flash.
    this.shadowRoot.getElementById('toggle')
      ?.addEventListener('click', () => this.toggleAttribute('open'));
  }
}

在几乎所有情况下,都应设置 shadowrootmode="open"(而非 closed),以便脚本和工具可以访问 this.shadowRoot 进行填充。如需在服务器上或测试中将现有 shadow root 序列化回 HTML,请使用 getHTML({ serializableShadowRoots: true }) 并使用 serializable: true 附加根。

避免 FOUC

只有在样式位于声明式模板内时,DSD 才能在首次绘制时正确呈现。为保留该信息,请执行以下操作:

  • 将组件 CSS 放在 <template shadowrootmode>(<style> 标记或 <link>)内,而不是放在单独的脚本加载工作表中。
  • 仅针对必须等待 JS 的样式(通常是页面级保护规则,例如在错过 SSR 传递的客户端渲染实例上淡出)与 my-card:not(:defined) { … } 配对。这些样式位于 light-DOM(页面)样式表中,因为 shadow root 尚不存在,且 :host 选择器无法从外部应用。DSD 模板中的结构样式不需要此类保护。
  • 请勿在水合时重新渲染影子树。将服务器标记视为权威标记,并为其附加行为。

后备策略

在不解析 shadowrootmode 的引擎中,模板处于非活动状态,其内容不会作为 shadow root 附加。检测此情况 (Object.hasOwn(HTMLTemplateElement.prototype, 'shadowRootMode')),如果不存在,则在升级期间强制从同一标记附加 shadow root,以便组件仍能呈现。