Предварительная отрисовка пользовательских элементов

Веб-функции: Декларативный Shadow DOM

Предварительная отрисовка означает отправку разметки пользовательского элемента, включая его теневое дерево, с сервера, чтобы компонент был стилизован и структурирован до запуска любого JavaScript. Основным примитивом платформы для этого является декларативный Shadow DOM (DSD): синтаксический анализатор прикрепляет теневой корень только из HTML, а элемент позже обновляется ("гидратируется") своим классом, если он зарегистрирован. Используйте этот шаблон для серверной отрисовки, потоковой передачи и предотвращения появления нестилизованного контента (FOUC).

Как создать декларативный корень теневого дерева

Вложите тег <template> с атрибутом shadowrootmode в качестве первого дочернего элемента хоста. Синтаксический анализатор прикрепляет содержимое шаблона в качестве теневого корня и удаляет шаблон. Скрипт не требуется.

<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 для гидратации. Чтобы сериализовать существующий теневой корень обратно в HTML на сервере или в тестах, используйте getHTML({ serializableShadowRoots: true }) и прикрепите корень с помощью serializable: true.

Как избежать FOUC

Декларативный Shadow DOM корректно отрисовывается при первой отрисовке, только если стили передаются внутри декларативного шаблона. Чтобы сохранить это:

  • Размещайте CSS-код компонента внутри <template shadowrootmode> (тег <style> или <link>), а не в отдельной таблице, загружаемой скриптом.
  • Используйте my-card:not(:defined) { … } только для стилей, которые должны дождаться JS. Обычно это правила защиты на уровне страницы, например затухание на экземплярах, отрисованных на стороне клиента, которые пропускают проход SSR. Они находятся в таблице стилей light-DOM (страницы), поскольку там ещё нет корневого элемента shadow, а селекторы :host не применяются извне. Для стилей структуры в шаблоне DSD такая защита не нужна.
  • Не выполняйте повторный рендеринг теневого дерева при гидратации. Рассматривать разметку сервера как авторитетную и привязывать к ней поведение.

Резервные стратегии

В системах, которые не обрабатывают shadowrootmode, шаблон инертен, а его контент не прикреплен как теневой корень. Обнаруживайте это (Object.hasOwn(HTMLTemplateElement.prototype, 'shadowRootMode')) и, если оно отсутствует, принудительно прикрепляйте теневой корень из той же разметки во время обновления, чтобы компонент по-прежнему отображался.