Pré-renderização de elementos personalizados

Recursos da Web: DOM paralelo declarativo

A pré-renderização significa enviar a marcação de um elemento personalizado, incluindo a árvore de sombra, do servidor para que o componente seja estilizado e estruturado antes da execução de qualquer JavaScript. A principal primitiva da plataforma para isso é o DOM de sombra declarativo (DSD): o analisador anexa a raiz de sombra apenas do HTML, e o elemento é atualizado ("hidratado") posteriormente pela classe, se houver uma registrada. Use esse padrão para renderização do lado do servidor, streaming e para evitar um flash de conteúdo sem estilo (FOUC, na sigla em inglês).

Como criar uma raiz de sombra declarativa

Aninhe um <template> com o atributo shadowrootmode como o primeiro filho do host. O analisador anexa o conteúdo como uma raiz de sombra e remove o modelo. Não é necessário nenhum script.

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

Tempo de hidratação

Quando a classe correspondente é registrada, o navegador faz upgrade do host. Como a raiz de sombra já existe, o construtor não pode criar uma segunda raiz sem verificar:

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'));
  }
}

Defina shadowrootmode="open" (não closed) para quase todos os casos, para que os scripts e as ferramentas possam alcançar this.shadowRoot para hidratação. Para serializar uma raiz de sombra atual de volta para HTML no servidor ou em testes, use getHTML({ serializableShadowRoots: true }) e anexe a raiz com serializable: true.

Como evitar o FOUC

O DSD só é renderizado corretamente na primeira pintura se os estilos forem enviados dentro do modelo declarativo. Para preservar isso:

  • Coloque o CSS do componente dentro do <template shadowrootmode> (uma tag <style> ou <link>), não em uma planilha separada carregada por script.
  • Combine com my-card:not(:defined) { … } apenas para estilos que precisam esperar pelo JS, normalmente regras de proteção no nível da página, como um fade-out em instâncias renderizadas pelo cliente que não passam pela SSR. Eles ficam na folha de estilo do light-DOM (página), já que a raiz de sombra ainda não existe lá e os seletores :host não se aplicam de fora. Os estilos estruturais dentro do modelo de DSD não precisam dessa proteção.
  • Não renderize novamente a árvore de sombra na hidratação. Trate a marcação do servidor como autoritativa e anexe o comportamento a ela.

Estratégias de fallback

Em mecanismos que não analisam shadowrootmode, o modelo é inerte e o conteúdo não é anexado como uma raiz de sombra. Detecte isso (Object.hasOwn(HTMLTemplateElement.prototype, 'shadowRootMode')) e, se estiver ausente, anexe a raiz de sombra de forma imperativa da mesma marcação durante o upgrade para que o componente ainda seja renderizado.