1 / 11
↓ clic para continuar

HTML Streaming

HTML que llega en pedazos al navegador

Conceptos básicos · agnóstico de stack

¿Qué es?

Una técnica para enviar HTML al navegador de forma incremental.

El server empieza a responder antes de tener todo el HTML, va emitiendo fragmentos (chunks) y el navegador los va pintando conforme llegan.

No es un estándar nuevo. Es HTTP/1.1 + chunked transfer + HTML.

Tradicional vs Streaming

🕰️ Tradicional

genera
envía todo
parsea y pinta

El navegador espera a tener el documento completo.

⚡ Streaming

envía
pinta
envía
pinta
envía
pinta

Cada fragmento se pinta en cuanto llega.

Clave: no es necesario esperar al </html> para empezar a mostrar contenido.

El truco: Transfer-Encoding: chunked

HTTP permite al server enviar una respuesta sin conocer el tamaño total por adelantado.

Formato de cada chunk (en bytes crudos):

1a <-- tamaño en HEX (26 bytes) <h1>Hola mundo</h1> <-- los datos b <p>Chunk 2</p> 0 <-- chunk vacío = fin de respuesta

Cada chunk es tamaño_hex + \n + datos + \n. El chunk 0 marca el final.

¿Cómo funciona?

Cliente Servidor │ │ │──── GET /messages ────────────────────────►│ │ │ │◄─── chunk 1: <article id="msg-1"> ────│ │◄─── chunk 2: <article id="msg-2"> ────│ │◄─── chunk 3: <!--ready--> ──────────│ │ │ (conexión abierta)◄─── chunk 4: <article id="msg-3"> ────│ │◄─── chunk 5: <article id="msg-4"> ────│ │ │ │ (heartbeat cada 15s) │ │◄─── chunk N: <!--ping--> ───────────

La conexión se mantiene abierta. El server "empuja" HTML al cliente sin que éste tenga que pedirlo.

En el cliente

fetch + ReadableStream: dos APIs nativas del navegador, ningún framework.

// 1. Abrir el stream
const res    = await fetch("/api/messages")
const reader = res.body.getReader()
const decoder = new TextDecoder("utf-8")
let buffer = ""

// 2. Leer hasta que el server cierre
while (true) {
  const { value, done } = await reader.read()
  if (done) break

  buffer += decoder.decode(value, { stream: true })

  // 3. Extraer cada <article> completo del buffer
  let start = buffer.indexOf("<article")
  while (start !== -1) {
    const end = buffer.indexOf("</article>", start)
    if (end === -1) break
    insertHTML(buffer.slice(start, end + 10))
    buffer = buffer.slice(end + 10)
    start = buffer.indexOf("<article")
  }
}

Insertar HTML de forma segura

function insertHTML(html) {
  const tpl = document.createElement("template")
  tpl.innerHTML = html                    // parsea, no ejecuta
  container.appendChild(tpl.content)  // se mueve al DOM real
}

¿Por qué es seguro?

HTML Streaming vs. WebSockets vs. SSE

HTML Streaming WebSockets SSE
Transporte HTTP/1.1 Upgrade a WS HTTP/1.1
Bidireccional No (server→client) No
Reconexión Manual Manual Automática
Frame Ninguno (HTML) Binario/Texto event:/data:
Browser API fetch + ReadableStream WebSocket EventSource
Complejidad Mínima Media Baja

HTML Streaming es el más simple: sólo HTTP y un loop de flush() en el server.

Casos de uso

Bueno para

  • Dashboards en tiempo real (métricas, logs).
  • Feeds (noticias, posts, comentarios).
  • Resultados de IA token a token.
  • UI server-driven sin SPA compleja.
  • Notificaciones / toasts en vivo.

No para

  • Chat bidireccional → WebSockets.
  • Juegos online (latencia muy baja) → WS / UDP.
  • Datos binarios grandes → WebSocket binario.
  • Cuando el cliente necesita pull explícito.

Limitaciones y caveats

En resumen

HTTP chunked + un loop de flush() en el server
+ fetch con ReadableStream en el cliente
= HTML Streaming.

Funciona con cualquier stack: no es propio de un framework, es de la web platform.

Listo, no hay más misterio. ✨