Streaming de HTML fora de ordem (OOO, na sigla em inglês)

O streaming de HTML fora de ordem (OOO, na sigla em inglês), também conhecido como atualizações parciais declarativas, é uma alternativa ao processamento linear tradicional de HTML de cima para baixo. Ele permite que os desenvolvedores definam marcadores de posição no início de um documento e os "corrijam" com conteúdo que chega mais tarde no mesmo stream de rede ou usando novas APIs de streaming imperativas do JavaScript.

Visão geral rápida

O recurso consiste em dois mecanismos principais:

  1. Patching declarativo (baseado em HTML): usa novas instruções de processamento (<?marker>, <?start>, <?end>) e <template for> para trocar conteúdo no DOM conforme ele é transmitido do servidor.
  2. Configuração e streaming imperativos (com base em JS): um conjunto de novos métodos (por exemplo, streamHTML(), setHTMLUnsafe()) que permitem definir HTML ou canalizar streams legíveis diretamente em elementos DOM com desempenho nativo e execução de script opcional.

Práticas recomendadas

  • Priorize o conteúdo acima da dobra: transmita primeiro a interface crítica (cabeçalho, imagem principal, navegação principal, conteúdo principal). Use marcadores de posição para componentes "caros" ou de menor prioridade, como comentários, anúncios ou conteúdo relacionado.
  • Implementar estados de carregamento nativos: use os marcadores <?start> e <?end> para envolver o conteúdo substituto. O navegador substitui automaticamente tudo entre esses marcadores quando o <template for> correspondente chega.
  • Minimizar a Cumulative Layout Shift (CLS): sempre forneça dimensões estáveis para os marcadores de posição. Se o tamanho final do conteúdo for desconhecido, use telas de esqueleto ou CSS aspect-ratio para reservar espaço e evitar mudanças bruscas.
  • Acessibilidade e gerenciamento de foco: lembre-se de que atualizações fora de ordem podem confundir os usuários de leitores de tela. Use regiões aria-live para atualizações críticas e garanta que o foco não seja perdido se um marcador de posição que contém o elemento ativo for substituído.

Detalhes de implementação

1. Marcadores HTML declarativos

O navegador procura instruções de processamento específicas para identificar onde o conteúdo precisa ser corrigido.

<!-- A simple insertion point -->
<div id="sidebar">
  <?marker name="dynamic-comments">
</div>

<!-- A replacement range with loading state -->
<main>
  <?start name="main-content">
    <div class="skeleton">Loading content...</div>
  <?end>
</main>

... (other content) ...

<!-- Later in the same stream, provide the content -->
<template for="dynamic-comments">
  <div class="comments">...</div>
</template>

<template for="main-content">
  <article>
    <h1>The Real Content</h1>
    <p>This replaces the skeleton div automatically.</p>
  </article>
</template>

Depois que um <template> substitui um ponto de inserção, esse ponto deixa de existir e não pode ser reutilizado. No entanto, um <template> pode conter pontos de inserção que podem ser usados posteriormente na transmissão.

 <?start name="replaced-content">
  <div class="skeleton">
<?end>
<!-- Later in the same stream, provide initial content -->
<template for="replaced-content">
  <?start name="replaced-content">
    <div>Quickly computed result</div>
  <?end>
</template>
<!-- Later in the same stream, provide full content -->
<template for="replaced-content">
  <?start name="replaced-content">
    <div>Full result</div>
  <?end>
</template>

2. Streaming de JS imperativo

O navegador oferece um conjunto de 24 métodos (6 ações, cada uma com 4 variações) para atualizar o DOM de forma estática (com uma string) ou por um stream.

Matriz de referência da API

Ação Estático (seguro) Streaming (seguro) Estático (não seguro) Streaming (não seguro)
Set setHTML() streamHTML() setHTMLUnsafe() streamHTMLUnsafe()
Substituir replaceWithHTML() streamReplaceWithHTML() replaceWithHTMLUnsafe() streamReplaceWithHTMLUnsafe()
Antes beforeHTML() streamBeforeHTML() beforeHTMLUnsafe() streamBeforeHTMLUnsafe()
Prepend prependHTML() streamPrependHTML() prependHTMLUnsafe() streamPrependHTMLUnsafe()
Anexar appendHTML() streamAppendHTML() appendHTMLUnsafe() streamAppendHTMLUnsafe()
Depois afterHTML() streamAfterHTML() afterHTMLUnsafe() streamAfterHTMLUnsafe()

Principais conceitos

  • Seguro x não seguro:
    • Os métodos seguros (por exemplo, setHTML()) usam um limpador integrado por padrão para remover conteúdo potencialmente perigoso, como tags <script>. É possível transmitir um objeto Sanitizer personalizado nas opções para aplicar além do limpador padrão.
    • Os métodos não seguros (por exemplo, setHTMLUnsafe()) não limpam por padrão, mas você ainda pode transmitir um Sanitizer personalizado. Para permitir que os scripts sejam executados no novo conteúdo, é necessário transmitir explicitamente { runScripts: true } nas opções. IMPORTANTE: use métodos não seguros apenas com conteúdo confiável em que os métodos seguros não funcionariam.
  • Estático x streaming:
    • Os métodos estáticos usam uma string (ou TrustedHTML) e a aplicam imediatamente.
    • Os métodos de streaming retornam um WritableStream. Isso permite que você envie conteúdo (por exemplo, de uma resposta fetch) diretamente para o DOM, e o navegador vai renderizá-lo de forma incremental à medida que os blocos chegarem.

Exemplo: como fazer streaming de uma resposta de busca

É possível canalizar as respostas de busca diretamente para o DOM sem processamento manual de blocos.

const main = document.querySelector('main');
const response = await fetch('/api/partial-update');

// Pipe the stream directly into the element
await response.body
  .pipeThrough(new TextDecoderStream())
  .pipeTo(main.streamHTMLUnsafe({ runScripts: true }));

// Or use the `textStream()` convenience method which streams directly without needing the intermediate `TextDecoderStream()` step
await response
  .textStream()
  .pipeTo(main.streamHTMLUnsafe({ runScripts: true }))

Matriz de referência de casos de uso

  • Arquitetura de ilha: hidrate componentes interativos independentes sobre HTML estático sem um ambiente de execução de framework completo.
  • Blocos "caros" adiados: entregue o shell de uma página imediatamente e transmita componentes com muitos dados do banco de dados conforme eles forem disponibilizados.
  • Atualizações de aplicativos de página única (SPA): transmita grandes transições de UI em SPAs usando streamHTML() em vez de esperar por um payload JSON completo e pela construção manual do DOM.

Antipadrões e avisos (NÃO FAÇA ISSO)

  • Não use o streaming OOO para tudo. A fragmentação excessiva de uma página pode aumentar a complexidade do stream e atrasar o conteúdo se o navegador gastar muito tempo corrigindo nós pequenos.
  • Não ignore os Tipos Confiáveis. Ao usar métodos *Unsafe em ambientes com Tipos Confiáveis aplicados, verifique se você está transmitindo um objeto de política limpo, se necessário.
  • Não presuma a ordem de execução do script. Os scripts dentro de um <template for> serão executados assim que forem corrigidos no documento. Não dependa da execução deles em uma sequência específica em relação a outros modelos adiados.
  • Não use sem uma estratégia de polyfill. Verifique se você tem um substituto ou use polyfills (html-setters-polyfill e template-for-polyfill) para oferecer suporte aos navegadores necessários até que esse recurso se torne Baseline.

Estratégias de fallback

É possível usar polyfills como template-for-polyfill e html-setters-polyfill para emular a superfície da API em navegadores que não oferecem suporte.

Os navegadores sem suporte a html-processing-instructions analisam <?marker>, <?start> e <?end> como comentários. O template-for-polyfill ainda reconhece esses comentários como marcadores. Por isso, mantenha a mesma sintaxe de instrução de processamento para todos os navegadores. Não reescreva os marcadores como comentários ou elementos HTML para navegadores incompatíveis.