12k
All articles

Atualizando uma página sem JavaScript

Envie atualizações HTML sem JavaScript usando template for e instruções de processamento. Veja o exemplo em Node, o suporte dos navegadores e as limitações.

OpenReplay Team
OpenReplay Team
Atualizando uma página sem JavaScript

Um elemento <template for="name"> preenche um placeholder marcado anteriormente no mesmo documento por uma processing instruction com esse nome. O servidor envia as seções lentas no final da mesma resposta HTML, e o parser do navegador as move para o lugar certo. Nenhum script é executado.

A maioria das páginas renderizadas no servidor tem uma região lenta, como uma contagem de pedidos ou uma verificação de status, e a solução habitual é uma div de carregamento mais um script que busca o fragmento depois. Esse script é a parte que muitas equipes gostariam de eliminar.

Este artigo mostra o markup, um servidor Node funcional que faz o streaming das atualizações, onde esse padrão se encaixa em relação ao htmx e o que os navegadores sem suporte renderizam.

Principais conclusões

  • <template for> substitui um intervalo <?start name="x">…<?end>, ou preenche um ponto de inserção <?marker name="x">, assim que o parser HTML chega até ele.
  • O servidor precisa fazer o flush da estrutura da página primeiro e escrever os templates depois, na mesma resposta. Qualquer camada que faça buffer da resposta inteira entrega o placeholder e seu substituto juntos.
  • O Chrome 150 habilita o streaming fora de ordem por padrão. Os métodos JavaScript complementares, como streamHTML, chegam no Chrome 155. Versões anteriores precisam de uma flag.
  • Navegadores sem suporte interpretam as processing instructions como comentários, de modo que o conteúdo de fallback permanece na tela.

O problema: buscar, localizar, substituir

Até agora, atualizar uma região de uma página renderizada no servidor exigia três etapas no cliente: buscar um fragmento, localizar o nó de destino e substituir seu conteúdo. Mesmo esta versão simplificada precisa de um script, uma segunda requisição e uma consulta ao DOM:

<div id="status">Loading…</div>
<script type="module">
  const res = await fetch('/status.html');
  document.querySelector('#status').innerHTML = await res.text();
</script>

O servidor já sabia o status enquanto renderizava a primeira resposta. O round trip extra existe apenas porque o HTML sempre foi interpretado na ordem do documento.

Como as atualizações parciais declarativas funcionam no markup

As atualizações parciais declarativas usam processing instructions como âncoras nomeadas e <template for> como o patch. O anúncio do Chrome sobre atualizações parciais declarativas define três âncoras. <?marker name="x"> é um ponto de inserção. <?start name="x"> e <?end> delimitam um intervalo que é substituído.

Se o placeholder exibe conteúdo de fallback, use um par <?start>…<?end>. Quando o template correspondente chega, tudo entre as duas instruções é substituído. Um <?marker> isolado apenas marca um ponto, então um texto de fallback colocado ao lado dele nunca é removido.

<div id="status"><?start name="status">Loading…<?end></div>

<!-- later in the same response -->
<template for="status">All systems <strong>normal</strong></template>

Depois que o template é interpretado, o DOM fica assim:

<div id="status">All systems <strong>normal</strong></div>

A documentação do Chrome explica por que foram escolhidas processing instructions em vez de elementos: um patch feito por meio delas não deixa nós extras para trás, o que significa que ele pode funcionar em lugares onde novos elementos quebrariam a estrutura, como o <head> (para alterar o <title>, por exemplo) ou o interior de uma <table>.

O que o servidor envia?

O servidor escreve a estrutura inteira imediatamente, mantém a resposta aberta enquanto aguarda os dados e então escreve cada <template for> antes de fechar o documento. O exemplo abaixo usa apenas node:http e ES modules. Salve-o como server.mjs e execute node server.mjs:

// server.mjs — run with: node server.mjs, then open http://localhost:3000
import { createServer } from 'node:http';

const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
const esc = (s) => String(s).replace(/[&<>"']/g, (c) =>
  ({ '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;', "'": '&#39;' }[c]));

async function getStatus() { await sleep(1000); return 'All systems normal'; }
async function* getOrders() {
  for (const o of ['Order #1001', 'Order #1002', 'Order #1003']) {
    await sleep(700);
    yield o;
  }
}

createServer(async (req, res) => {
  if (req.url !== '/') { res.writeHead(404).end(); return; }

  res.writeHead(200, {
    'Content-Type': 'text/html; charset=utf-8',
    'Cache-Control': 'no-store',
  });

  // 1. Flush the shell immediately.
  res.write(`<!doctype html>
<html><head><meta charset="utf-8"><title>Orders</title></head>
<body>
  <h1>Your orders</h1>
  <p id="status"><?start name="status">Checking status…<?end></p>
  <ul id="orders"><?start name="orders"><li>Loading orders…</li><?end></ul>
`);

  // 2. Templates are direct children of <body>, so they can reach the placeholders above.
  const status = await getStatus();
  res.write(`<template for="status">${esc(status)}</template>\n`);

  // 3. Each template appends one item and leaves a fresh marker for the next.
  for await (const order of getOrders()) {
    res.write(`<template for="orders"><li>${esc(order)}</li><?marker name="orders"></template>\n`);
  }

  // 4. Close the document only after the last template.
  res.end('</body></html>');
}).listen(3000);

Cada chamada a response.write() envia um chunk do corpo sem esperar pelo end(). Para entender melhor como consumir saída em chunks, veja o artigo web streams explained. As chamadas a sleep simulam consultas lentas. Elas não são um benchmark.

O helper esc() existe porque o conteúdo do template é HTML comum. Qualquer dado interpolado pode injetar markup, a menos que você faça o escape.

Quando o stream termina, #orders contém três elementos <li> seguidos de um <?marker name="orders"> final. Abra a página no Chrome 150 ou superior para ver a lista sendo preenchida.

O buffering anula o streaming fora de ordem

O streaming fora de ordem só ajuda se os bytes chegarem ao navegador ao longo do tempo. Três camadas comuns retêm a resposta inteira: middleware de compressão que espera o buffer encher, proxies reversos ou CDNs com buffering de resposta ativado e frameworks que renderizam a página inteira antes de enviar qualquer coisa. Execute curl -N http://localhost:3000 passando por cada camada para verificar se os chunks chegam separadamente. A flag -N desativa o buffering de saída do curl. O session replay mostra o resultado do ponto de vista do usuário, por exemplo, uma tela em branco que permanece vazia até que a página inteira chegue de uma só vez porque um proxy reteve o stream.

Casos de uso e seus limites

O patching declarativo só funciona enquanto o servidor ainda está escrevendo o documento, portanto o tempo de vida da resposta é a janela de atualização.

  • Lista dinâmica. Terminar cada template com um novo <?marker> de mesmo nome permite que templates posteriores continuem adicionando itens naquele ponto. A lista só continua dinâmica enquanto a conexão permanecer aberta.
  • Linha de status. Um único intervalo <?start>/<?end> com um template, como no exemplo acima.
  • Resultado de formulário. Uma navegação POST comum retorna uma página cuja estrutura é renderizada de imediato e cujo resultado chega via streaming quando o processamento termina.

Tudo o que reage a cliques depois que a página foi carregada (filtros, edições inline, UI otimista) ainda precisa de estado no cliente, JavaScript ou uma biblioteca.

Atualizações parciais declarativas vs htmx

O htmx envia novas requisições HTTP em resposta a eventos do usuário e insere os fragmentos retornados na página. <template for> funciona apenas dentro da resposta que já está em streaming. Depois que o servidor encerra essa resposta, novas atualizações exigem JavaScript ou uma nova navegação.

O htmx continua sendo uma biblioteca JavaScript. O htmx 4.0 adiciona uma tag <hx-partial>, que tem como alvo vários elementos a partir de uma única resposta. Padrões como scroll infinito com htmx, em que um elemento de carregamento que entra na área visível faz o htmx buscar a próxima página no seu servidor, e validação de formulários com htmx dependem de requisições disparadas pelo usuário.

htmx<template for>
GatilhoEvento do usuárioOrdem de escrita do servidor
RequisiçõesUma por interaçãoUma resposta
Executado emBiblioteca JavaScriptParser HTML
AlvosAtributos com seletores CSSProcessing instructions nomeadas
Atualizações após o fim da respostaSimNão

Quando você precisa de JavaScript de qualquer forma

Para conteúdo que chega após o carregamento, o Chrome está adicionando métodos complementares como streamHTML e appendHTML, além de variantes Unsafe, e estendendo o setHTML já existente. Os desenvolvedores podem testá-los desde o Chrome 148 por meio de chrome://flags/#enable-experimental-web-platform-features, e eles estão incluídos no Chrome 155, que entrou em beta em 16 de setembro de 2026.

const el = document.querySelector('#feed');
const response = await fetch('/feed-fragment.html');
await response.textStream().pipeTo(el.streamHTMLUnsafe());

response.textStream() está disponível a partir do Chrome 151. Em versões mais antigas, use response.body.pipeThrough(new TextDecoderStream()) no lugar.

Quando você faz o pipe de markup através de streamHTMLUnsafe, qualquer <template for> contido nele aplica patches na página ativa. setHTML e innerHTML funcionam de outra forma: eles primeiro interpretam o conteúdo em um fragmento separado, então os templates dentro deles só conseguem aplicar patches em nós desse fragmento, e não na página existente.

Suporte dos navegadores, fallback e restrições

O streaming fora de ordem com <template for> e as processing instructions <?marker>, <?start> e <?end> está ativado por padrão a partir do Chrome 150, que chegou à versão estável em 30 de junho de 2026. O recurso é acompanhado no ChromeStatus, e o atributo for faz parte do WHATWG HTML Living Standard. Nenhum navegador fora do ecossistema Chromium o lançou. Na thread de standards-positions do WebKit, um engenheiro do WebKit propôs definir a posição do WebKit como “support”.

O Intent to Ship confirma que navegadores sem suporte interpretam as processing instructions como comentários, de modo que qualquer conteúdo de fallback permanece na tela. O conteúdo de um elemento <template> padrão não é renderizado, portanto os dados enviados via streaming nunca aparecem nesses navegadores.

Chrome 150+Sem suporteCom template-for-polyfill
Texto de fallbackSubstituídoPermanece visívelSubstituído
Conteúdo via streamingAplicado via patchOculto no <template>Aplicado via patch, com limitações do parser

O Chrome publica o template-for-polyfill no npm. Ele cobre os casos mais comuns, mas não tem acesso ao parser nativo do navegador, então algumas limitações permanecem. Ele também é JavaScript, portanto uma página “sem JS” que precise funcionar em todos os navegadores ainda depende de um script. O html-setters-polyfill, um projeto separado, reproduz os nomes dos métodos das novas APIs, mas coleta todo o conteúdo primeiro e o insere no final, então não faz streaming.

Restrições do <template for> que você precisa conhecer:

  • Um template só consegue alcançar processing instructions que compartilham o mesmo elemento pai. Posicione os templates diretamente sob o <body> e eles poderão alcançar o documento inteiro, incluindo o <head>.
  • Você pode omitir o <?end>. Nesse caso, a substituição vai do <?start> até o fechamento do elemento pai.
  • Se você mover uma processing instruction enquanto um template ainda está fazendo streaming para ela, o restante do conteúdo pode continuar sendo inserido no local antigo.

Conclusão

O streaming fora de ordem transfere a rotina de buscar, localizar e substituir para o parser HTML, mas apenas dentro de uma única resposta aberta. Onde quer que você o utilize, coloque os dados reais dentro de intervalos de fallback <?start>/<?end> para que navegadores sem suporte ainda exibam algo coerente. Em seguida, execute o server.mjs por trás do seu proxy e da sua CDN reais para verificar se os chunks chegam separadamente antes de depender disso em produção.

Perguntas frequentes

Qual é a diferença entre template for e o streaming com React Suspense?

O SSR com streaming do React envia cada boundary de Suspense resolvido como uma div oculta, seguida de um script inline que a coloca no lugar do fallback. Se os scripts estiverem desativados, ou se uma Content Security Policy bloquear scripts inline sem nonce, esse conteúdo nunca é movido para o lugar certo. Com template for, o próprio parser HTML faz a substituição, então nenhum script é executado e a página não precisa de permissão para scripts inline.

Existe uma forma compatível com todos os navegadores de fazer streaming de HTML fora de ordem sem JavaScript?

Sim. O Declarative Shadow DOM com slots nomeados funciona no Chrome, no Firefox e no Safari. A estrutura da página é um template com shadowrootmode='open' que contém elementos slot com conteúdo de fallback. Elementos com um atributo slot correspondente chegam depois via streaming e preenchem esses slots. A desvantagem é que o shadow root normalmente fica no body, então o CSS do documento não estiliza o markup da estrutura dentro da shadow tree.

O que acontece se uma consulta lenta falhar depois que a estrutura da página já foi enviada?

O status HTTP e os headers ficam definidos assim que o primeiro chunk é escrito, então o servidor não pode mais mudar para um erro 500, um redirecionamento ou novos cookies. Trate a falha no markup: escreva um template com o mesmo nome contendo uma mensagem de erro, e ele substituirá o fallback de carregamento. No Node, chamar res.setHeader depois que os headers foram enviados lança ERR_HTTP_HEADERS_SENT.

Open-source session replay

Complete picture for complete understanding

Capture every clue your frontend is leaving so you can instantly get to the root cause of any issue with OpenReplay — the open-source session replay tool for developers. Self-host it in minutes, and have complete control over your customer data.

Star on GitHub12k

We use cookies to improve your experience. By using our site, you accept cookies.