Предварительная отрисовка означает отправку разметки пользовательского элемента, включая его теневое дерево, с сервера, чтобы компонент был стилизован и структурирован до запуска любого 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')) и, если оно отсутствует, принудительно прикрепляйте теневой корень из той же разметки во время обновления, чтобы компонент по-прежнему отображался.