カスタム要素の事前レンダリング

ウェブ機能: 宣言型 Shadow DOM

プリレンダリングとは、カスタム要素のマークアップ(シャドウツリーを含む)をサーバーから送信することを意味します。これにより、JavaScript が実行される前にコンポーネントのスタイルと構造が設定されます。このための主なプラットフォーム プリミティブは、宣言型シャドウ DOM(DSD)です。パーサーは HTML のみからシャドウルートをアタッチし、要素は後でクラスによってアップグレード(「ハイドレート」)されます(登録されている場合)。このパターンは、サーバーサイド レンダリング、ストリーミング、スタイルが適用されていないコンテンツのフラッシュ(FOUC)の回避に使用します。

宣言型シャドウルートの作成

shadowrootmode 属性を持つ <template> をホストの最初の子としてネストします。パーサーは、そのコンテンツをシャドウルートとしてアタッチし、テンプレートを削除します。スクリプトは必要ありません。

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

水分補給のタイミング

一致するクラスが登録されると、ブラウザはホストをアップグレードします。シャドウルートはすでに存在しているため、コンストラクタは 2 つ目のシャドウルートを無条件に作成してはなりません。

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 にアクセスしてハイドレートできるようになります。サーバー上またはテストで既存のシャドウルートを HTML にシリアル化するには、getHTML({ serializableShadowRoots: true }) を使用して、serializable: true でルートをアタッチします。

FOUC の回避

DSD が最初の描画で正しくレンダリングされるのは、スタイルが宣言型テンプレート内で提供されている場合のみです。これを維持するには、次の操作を行います。

  • コンポーネントの CSS は、スクリプトで読み込まれる別のシートではなく、<template shadowrootmode>(<style> タグまたは <link>)の内部に配置します。
  • my-card:not(:defined) { … } と組み合わせるのは、JS を待つ必要があるスタイルのみにしてください。通常は、SSR パスを逃したクライアント レンダリング インスタンスのフェードアウトなど、ページレベルのガードルールです。これらは、light-DOM(ページ)スタイルシートにあります。シャドウルートがまだ存在せず、:host セレクタが外部から適用されないためです。DSD テンプレート内の構造スタイルには、このようなガードは必要ありません。
  • ハイドレーション時にシャドウツリーを再レンダリングしないでください。サーバー マークアップを信頼できるものとして扱い、動作を関連付けます。

代替戦略

shadowrootmode を解析しないエンジンでは、テンプレートは不活性であり、そのコンテンツはシャドウルートとしてアタッチされません。これを検出し(Object.hasOwn(HTMLTemplateElement.prototype, 'shadowRootMode'))、存在しない場合は、アップグレード中に同じマークアップからシャドウルートを強制的にアタッチして、コンポーネントがレンダリングされるようにします。