{"schemaVersion":1,"id":"https://lucioduran.com/es/blog/streaming-html-out-of-order-island-architecture","url":"https://lucioduran.com/es/blog/streaming-html-out-of-order-island-architecture","title":"Streaming HTML: Out-of-Order Streaming, Island Architecture e Hidratación Selectiva","description":"Un análisis de estrategias de streaming HTML con Marko, Qwik y Astro, out-of-order streaming, los internals de island architecture, y por qué la hidratación progresiva cambia todo en performance web.","language":"es","datePublished":"2026-01-03","dateModified":null,"contentFormat":"text/markdown","content":"Frontend\n\n# Streaming HTML: Out-of-Order Streaming, Island Architecture e Hidratación Selectiva\n\n2026-01-039 min lectura\n\nLucio Durán\nEngineering Manager & AI Solutions Architect\n\nTambién disponible en: [English](<https://lucioduran.com/blog/streaming-html-out-of-order-island-architecture>)\n\n## Cómo el SSR Tradicional te Falla\n\nEn server-side rendering tradicional, el ciclo de vida es así:\n\n```\nEl browser pide la página\n → El servidor recibe el request\n → El servidor fetchea TODOS los datos (espera al más lento)\n → El servidor renderiza el HTML completo\n → El servidor manda la respuesta completa\n → El browser empieza a parsear\n → El browser descarga el JS\n → Corre la hidratación\n → La página se vuelve interactiva\n```\n\nCada paso es secuencial. El browser no recibe nada hasta que el servidor tiene todo. Esto es fundamentalmente malo para la experiencia del usuario porque se queda mirando una pantalla en blanco mientras el servidor espera una query lenta a la base o una API de terceros.\n\n## Entra el Streaming HTML\n\nHTTP/1.1 soporta chunked transfer encoding desde 1999. La idea es simple: en vez de buffear toda la respuesta, mandá chunks a medida que estén disponibles.\n\n```javascript\n// Node.js streaming response - lo básico\nimport { Readable } from 'stream';\n\napp.get('/product/:id', async (req, res) => {\n res.setHeader('Content-Type', 'text/html');\n res.setHeader('Transfer-Encoding', 'chunked');\n\n // Mandá el shell inmediatamente\n res.write(`\n <!DOCTYPE html>\n <html>\n <head><title>Producto</title><link rel=\"stylesheet\" href=\"/styles.css\"></head>\n <body>\n <header><!-- el nav renderiza inmediatamente --></header>\n <main>\n `);\n\n // Los detalles del producto vienen rápido - mandalos\n const product = await fetchProduct(req.params.id); // ~180ms\n res.write(`\n <h1>${product.name}</h1>\n <p class=\"price\">${product.price}</p>\n <img src=\"${product.image}\" alt=\"${product.name}\">\n <div id=\"reviews-placeholder\">Cargando reviews...</div>\n <div id=\"recommendations-placeholder\">Cargando recomendaciones...</div>\n `);\n\n // Disparamos los requests lentos en paralelo\n const [reviews, recommendations] = await Promise.all([\n fetchReviews(req.params.id), // ~600ms\n fetchRecommendations(req.params.id) // ~1200ms\n ]);\n\n res.write(`\n <script>\n document.getElementById('reviews-placeholder').outerHTML =\n '${renderReviews(reviews)}';\n document.getElementById('recommendations-placeholder').outerHTML =\n '${renderRecommendations(recommendations)}';\n </script>\n `);\n\n res.end('</main></body></html>');\n});\n```\n\nEsto es rudimentario pero ilustrativo. El browser recibe el header y la info del producto en 200ms y puede empezar a renderizar. Las reviews y recomendaciones gotean después. Pero hay un problema: seguimos esperando las reviews antes de mandar las recomendaciones, aunque las recomendaciones podrían terminar primero.\n\n## Out-of-Order Streaming\n\nAcá resulta interesante. El out-of-order streaming desacopla el orden de resolución de datos del orden de aparición en el DOM. El servidor manda elementos placeholder, y después los reemplaza con contenido real a medida que cada fuente de datos se resuelve, en el orden que sea que terminen.\n\nMarko viene haciendo esto desde 2019. El modelo mental es así:\n\n```html\n<!-- Lo que el servidor manda PRIMERO (inmediatamente) -->\n<div id=\"product-shell\">\n <h1>Cargando...</h1>\n</div>\n<div id=\"reviews-slot\" hidden>\n <!-- Placeholder, se va a swappear -->\n</div>\n<div id=\"recs-slot\" hidden>\n <!-- Placeholder, se va a swappear -->\n</div>\n\n<!-- Script inline para reemplazo out-of-order -->\n<script>\n window.$RS = function(slotId, html) {\n const slot = document.getElementById(slotId);\n const template = document.createElement('template');\n template.innerHTML = html;\n slot.replaceWith(template.content);\n };\n</script>\n\n<!-- Lo que el servidor manda cuando los datos del producto se resuelven (~180ms) -->\n<script>$RS('product-shell', '<h1>Nike Air Max 90</h1><p>$129.99</p>')</script>\n\n<!-- Lo que el servidor manda cuando las recomendaciones se resuelven (~800ms) -->\n<!-- Ojo: esto llega ANTES que las reviews aunque está después en el DOM -->\n<script>$RS('recs-slot', '<div class=\"recs\">...</div>')</script>\n\n<!-- Lo que el servidor manda cuando las reviews finalmente se resuelven (~1200ms) -->\n<script>$RS('reviews-slot', '<div class=\"reviews\">...</div>')</script>\n```\n\nLo hermoso es que el browser procesa cada chunk a medida que llega. El usuario ve el producto casi al instante, después aparecen las recomendaciones, después las reviews. No se necesita un framework de JavaScript para este render inicial, solo scripts inline y manipulación del DOM.\n\n### La Implementación de Marko\n\nEl approach de Marko al out-of-order streaming es el más maduro. Usa un tag ` <await> ` que crea streaming boundaries:\n\n```marko\n// product-page.marko\n<layout>\n <@body>\n <await(productPromise)>\n <@then|product|>\n <product-details product=product />\n </@then>\n <@placeholder>\n <product-skeleton />\n </@placeholder>\n </await>\n\n <await(reviewsPromise) client-reorder>\n <@then|reviews|>\n <review-list reviews=reviews />\n </@then>\n <@placeholder>\n <reviews-skeleton />\n </@placeholder>\n </await>\n\n <await(recommendationsPromise) client-reorder>\n <@then|recs|>\n <recommendation-carousel items=recs />\n </@then>\n <@placeholder>\n <recs-skeleton />\n </@placeholder>\n </await>\n </@body>\n</layout>\n```\n\nEl atributo ` client-reorder ` es la clave. Sin él, Marko streamea en orden del documento (esperando a los await blocks anteriores antes de los posteriores). Con él, cada bloque se resuelve independientemente, y Marko inyecta un runtime diminuto (~1KB) para manejar los DOM swaps.\n\n### El Approach de React con Suspense\n\nReact 18+ soporta streaming SSR con Suspense boundaries:\n\n```jsx\n// ProductPage.jsx\nimport { Suspense } from 'react';\nimport { renderToPipeableStream } from 'react-dom/server';\n\nfunction ProductPage({ productId }) {\n return (\n <Layout>\n <Suspense fallback={<ProductSkeleton />}>\n <ProductDetails id={productId} />\n </Suspense>\n\n <Suspense fallback={<ReviewsSkeleton />}>\n <ReviewList id={productId} />\n </Suspense>\n\n <Suspense fallback={<RecommendationsSkeleton />}>\n <Recommendations id={productId} />\n </Suspense>\n </Layout>\n );\n}\n\n// Server entry\napp.get('/product/:id', (req, res) => {\n const { pipe } = renderToPipeableStream(\n <ProductPage productId={req.params.id} />,\n {\n bootstrapScripts: ['/client.js'],\n onShellReady() {\n res.setHeader('Content-Type', 'text/html');\n pipe(res);\n },\n onError(error) {\n console.error(error);\n res.statusCode = 500;\n }\n }\n );\n});\n```\n\nEl streaming de React funciona, pero viene con un costo: teners que hidratar el árbol de componentes entero. Aunque solo dos componentes sean interactivos, React descarga y re-ejecuta todo. Acá es donde entra la island architecture.\n\n## Island Architecture: Interactividad Selectiva\n\nEl término \"island architecture\" viene del blog post de Jason Miller de 2020, aunque el concepto existía de antes. La idea: una página es un mar de HTML estático con islas de interactividad. Solo las islas envían JavaScript.\n\n### La Implementación de Astro\n\n[Astro](<https://astro.build/>) es el referente de island architecture:\n\n```astro\n---\n// product-page.astro\nimport Layout from '../layouts/Layout.astro';\nimport ProductDetails from '../components/ProductDetails.astro'; // ¡Estático!\nimport ReviewList from '../components/ReviewList.astro'; // ¡Estático!\nimport AddToCart from '../components/AddToCart.tsx'; // ¡Isla!\nimport ImageCarousel from '../components/ImageCarousel.svelte'; // ¡Isla!\n\nconst product = await fetchProduct(Astro.params.id);\nconst reviews = await fetchReviews(Astro.params.id);\n---\n\n<Layout title={product.name}>\n <!-- HTML estático, cero JS -->\n <ProductDetails product={product} />\n\n <!-- Isla interactiva: carga cuando es visible en el viewport -->\n <ImageCarousel client:visible images={product.images} />\n\n <!-- Isla interactiva: carga inmediatamente (above the fold, crítico) -->\n <AddToCart client:load productId={product.id} price={product.price} />\n\n <!-- HTML estático, cero JS -->\n <ReviewList reviews={reviews} />\n</Layout>\n```\n\nLas directivas ` client:* ` controlan cuándo carga el JavaScript de cada isla:\n\n```typescript\n// Estrategias de carga de islas en Astro\nclient:load // Carga e hidrata inmediatamente al cargar la página\nclient:idle // Carga después de la carga inicial (requestIdleCallback)\nclient:visible // Carga cuando el componente entra al viewport (IntersectionObserver)\nclient:media // Carga cuando se cumple un CSS media query\nclient:only // Saltea SSR, solo render en cliente\n```\n\n### Qwik: Más Allá de las Islas\n\n[Qwik](<https://qwik.dev/>) toma un enfoque fundamentalmente diferente. En vez de hidratación (re-ejecutar código de componentes en el cliente), Qwik usa resumabilidad. El servidor serializa el estado de los componentes y los bindings de eventos en el HTML, y el cliente literalmente retoma de donde dejó el servidor.\n\n```tsx\n// product-page.tsx (Qwik)\nimport { component$, useSignal, $ } from '@builder.io/qwik';\nimport { routeLoader$ } from '@builder.io/qwik-city';\n\nexport const useProductData = routeLoader$(async ({ params }) => {\n const [product, reviews] = await Promise.all([\n fetchProduct(params.id),\n fetchReviews(params.id)\n ]);\n return { product, reviews };\n});\n\nexport default component$(() => {\n const data = useProductData();\n const quantity = useSignal(1);\n\n // Este handler se carga lazy SOLO cuando el usuario clickea\n const addToCart = $(() => {\n cart.add(data.value.product.id, quantity.value);\n });\n\n return (\n <div>\n <h1>{data.value.product.name}</h1>\n <p class=\"price\">${data.value.product.price}</p>\n\n <div class=\"quantity-selector\">\n <button onClick$={() => quantity.value = Math.max(1, quantity.value - 1)}>-</button>\n <span>{quantity.value}</span>\n <button onClick$={() => quantity.value++}>+</button>\n </div>\n\n <button onClick$={addToCart}>Agregar al Carrito</button>\n\n {data.value.reviews.map(review => (\n <div key={review.id} class=\"review\">\n <p>{review.text}</p>\n <span>{'★'.repeat(review.rating)}</span>\n </div>\n ))}\n </div>\n );\n});\n```\n\nEl marcador ` $() ` le dice al optimizador de Qwik dónde crear boundaries de lazy-loading. Cada ` $ ` se convierte en un chunk separado que solo se descarga cuando se dispara. La carga inicial de la página envía prácticamente cero JavaScript. El click handler de \"Agregar al Carrito\" se fetchea de la red solo cuando el usuario clickea el botón.\n\nObservar cómo se ve el HTML output de Qwik:\n\n```html\n<button on:click=\"product-page_component_addtocart_0lz8qwik.js#s_addToCart\"\n q:id=\"3\">\n Agregar al Carrito\n</button>\n```\n\nEse atributo ` on:click ` contiene una URL al click handler. Cuando el usuario clickea, el runtime diminuto (~1KB) de Qwik fetchea ese script, deserializa el estado y ejecuta el handler. Sin hidratación. Sin re-renderizar todo el árbol. Solo el handler.\n\n## Comparación de Performance: Números Reales\n\nBenchmarkeé la misma página de producto con tres enfoques en un dataset real de e-commerce. Mismo servidor (AWS t3.medium), mismas latencias de API, mismo contenido:\n\n```\n┌────────────────────┬─────────┬───────────┬──────────────┬───────────┐\n│ Métrica │ React │ Astro │ Qwik │ Marko │\n│ │ SSR │ Islands │ Resumable │ Streaming │\n├────────────────────┼─────────┼───────────┼──────────────┼───────────┤\n│ TTFB │ 1820ms │ 420ms │ 390ms │ 180ms │\n│ FCP │ 2100ms │ 520ms │ 480ms │ 240ms │\n│ LCP │ 2400ms │ 680ms │ 590ms │ 520ms │\n│ TTI │ 3800ms │ 710ms │ 490ms* │ 1200ms │\n│ JS transferido │ 187KB │ 24KB │ 2KB inicial │ 42KB │\n│ Total blocking │ 340ms │ 45ms │ 0ms │ 120ms │\n└────────────────────┴─────────┴───────────┴──────────────┴───────────┘\n```\n\nMarko gana en TTFB porque su out-of-order streaming manda el primer byte casi inmediatamente. Qwik gana en TTI porque efectivamente no hay costo de hidratación. Astro queda en un punto óptimo para páginas heavy en contenido. React SSR sin streaming es el baseline que todos tratan de superar.\n\n## Cuándo Usar Qué\n\nDespués de construir sitios en producción con los tres enfoques, aquí va mi evaluación honesta:\n\n**Usar Astro** cuando tu sitio es primariamente contenido con toques de interactividad. Blogs, documentación, sitios de marketing, portfolios. El modelo de islas mapea perfecto a estos case es de uso.\n\n**Usar Qwik** cuando estés construyendo una aplicación completa, dashboards, e-commerce, productos SaaS, donde cada componente es potencialmente interactivo, pero se desea performance nivel MPA. La curva de aprendizaje de la sintaxis ` $ ` es real, y el ecosistema es más chico, pero las características de performance son notables.\n\n**Usar Marko** si se está a escala eBay y se necesita la implementación de streaming más battle-tested. Marko viene streameando HTML en producción hace años en eBay, manejando millones de requests. El ecosistema afuera de eBay es chico, que es su mayor limitación.\n\n**Usar React con streaming** si tu equipo ya sabe React y no es posible justificar una migración de framework. El streaming SSR de React con Suspense es una mejora masiva sobre el enfoque con buffer, aunque no matchee la eficiencia cruda de las alternativas.\n\nLa industria está convergiendo en un modelo donde el servidor hace la mayor parte del trabajo de rendering, y el cliente solo maneja interactividad genuina. Ya sea que lo llamen islas, resumabilidad o hidratación selectiva, la dirección es clara. La era de enviar 300KB de JavaScript para renderizar una tarjeta de producto se está terminando, y es un cambio bienvenido.\n\n## Preguntas frecuentes\n\n### ¿Qué es el out-of-order streaming?\n\nEs una técnica donde el servidor envía chunks de HTML a medida que están listos, sin importar su posición en el DOM. Se renderiza un placeholder primero, que después se reemplaza con contenido real via scripts inline cuando los datos se resuelven. Marko fue pionero en este approach.\n\n### ¿En qué se diferencia island architecture de partial hydration?\n\nIsland architecture trata los componentes interactivos como islas aisladas en un mar de HTML estático. Partial hydration es el mecanismo que hace funcionar las islas, solo el código de la isla se descarga y ejecuta, no el JavaScript de toda la página.\n\n### ¿Debería usar Marko, Qwik o Astro para un proyecto nuevo?\n\nAstro para sitios heavy en contenido con toques de interactividad. Qwik para aplicaciones completas donde se desea feel de SPA con performance de MPA. Marko si se está en el ecosistema de eBay o se desea la implementación de streaming más madura.\n\n## Tips\n\n1. **Tip:** El out-of-order streaming solo ayuda si tu shell renderiza rápido. Colocar los ` <Suspense> ` boundaries alrededor de data fetches lentos, no alrededor de componentes de layout.\n2. **Tip:** Con islas en Astro, utilizar ` client:visible ` para componentes interactivos below-the-fold y ` client:idle ` para UI no crítica como widgets de analytics.\n3. **Tip:** La resumabilidad de Qwik elimina la hidratación por completo, pero solo si evitás ` useVisibleTask$ ` que fuerza ejecución eager. Tratalo como escape hatch, no como default.\n4. **Tip:** Probar streaming con ` curl --no-buffer ` para ver realmente los chunks llegando. El DevTools del browser comprime el stream y oculta el comportamiento real.\n\n- streaming-html\n- island-architecture\n- progressive-hydration\n- marko\n- qwik\n- astro\n- web-performance\n\n[Volver al Blog](<https://lucioduran.com/es/blog>)","headings":[{"level":1,"text":"Streaming HTML: Out-of-Order Streaming, Island Architecture e Hidratación Selectiva","url":"https://lucioduran.com/es/blog/streaming-html-out-of-order-island-architecture#title"},{"level":2,"text":"Cómo el SSR Tradicional te Falla","url":"https://lucioduran.com/es/blog/streaming-html-out-of-order-island-architecture#cmo-el-ssr-tradicional-te-falla"},{"level":2,"text":"Entra el Streaming HTML","url":"https://lucioduran.com/es/blog/streaming-html-out-of-order-island-architecture#entra-el-streaming-html"},{"level":2,"text":"Out-of-Order Streaming","url":"https://lucioduran.com/es/blog/streaming-html-out-of-order-island-architecture#out-of-order-streaming"},{"level":3,"text":"La Implementación de Marko","url":"https://lucioduran.com/es/blog/streaming-html-out-of-order-island-architecture#la-implementacin-de-marko"},{"level":3,"text":"El Approach de React con Suspense","url":"https://lucioduran.com/es/blog/streaming-html-out-of-order-island-architecture#el-approach-de-react-con-suspense"},{"level":2,"text":"Island Architecture: Interactividad Selectiva","url":"https://lucioduran.com/es/blog/streaming-html-out-of-order-island-architecture#island-architecture-interactividad-selectiva"},{"level":3,"text":"La Implementación de Astro","url":"https://lucioduran.com/es/blog/streaming-html-out-of-order-island-architecture#la-implementacin-de-astro"},{"level":3,"text":"Qwik: Más Allá de las Islas","url":"https://lucioduran.com/es/blog/streaming-html-out-of-order-island-architecture#qwik-ms-all-de-las-islas"},{"level":2,"text":"Comparación de Performance: Números Reales","url":"https://lucioduran.com/es/blog/streaming-html-out-of-order-island-architecture#comparacin-de-performance-nmeros-reales"},{"level":2,"text":"Cuándo Usar Qué","url":"https://lucioduran.com/es/blog/streaming-html-out-of-order-island-architecture#cundo-usar-qu"},{"level":2,"text":"Preguntas frecuentes","url":"https://lucioduran.com/es/blog/streaming-html-out-of-order-island-architecture#questions"},{"level":3,"text":"¿Qué es el out-of-order streaming?","url":"https://lucioduran.com/es/blog/streaming-html-out-of-order-island-architecture#question-1"},{"level":3,"text":"¿En qué se diferencia island architecture de partial hydration?","url":"https://lucioduran.com/es/blog/streaming-html-out-of-order-island-architecture#question-2"},{"level":3,"text":"¿Debería usar Marko, Qwik o Astro para un proyecto nuevo?","url":"https://lucioduran.com/es/blog/streaming-html-out-of-order-island-architecture#question-3"},{"level":2,"text":"Tips","url":"https://lucioduran.com/es/blog/streaming-html-out-of-order-island-architecture#article-tips"}],"sections":[{"heading":"Streaming HTML: Out-of-Order Streaming, Island Architecture e Hidratación Selectiva","level":1,"url":"https://lucioduran.com/es/blog/streaming-html-out-of-order-island-architecture","content":"Frontend"},{"heading":"Streaming HTML: Out-of-Order Streaming, Island Architecture e Hidratación Selectiva","level":1,"url":"https://lucioduran.com/es/blog/streaming-html-out-of-order-island-architecture#title","content":"2026-01-039 min lectura\n\nLucio Durán\nEngineering Manager & AI Solutions Architect\n\nTambién disponible en: [English](<https://lucioduran.com/blog/streaming-html-out-of-order-island-architecture>)"},{"heading":"Cómo el SSR Tradicional te Falla","level":2,"url":"https://lucioduran.com/es/blog/streaming-html-out-of-order-island-architecture#cmo-el-ssr-tradicional-te-falla","content":"En server-side rendering tradicional, el ciclo de vida es así:\n\n```\nEl browser pide la página\n → El servidor recibe el request\n → El servidor fetchea TODOS los datos (espera al más lento)\n → El servidor renderiza el HTML completo\n → El servidor manda la respuesta completa\n → El browser empieza a parsear\n → El browser descarga el JS\n → Corre la hidratación\n → La página se vuelve interactiva\n```\n\nCada paso es secuencial. El browser no recibe nada hasta que el servidor tiene todo. Esto es fundamentalmente malo para la experiencia del usuario porque se queda mirando una pantalla en blanco mientras el servidor espera una query lenta a la base o una API de terceros."},{"heading":"Entra el Streaming HTML","level":2,"url":"https://lucioduran.com/es/blog/streaming-html-out-of-order-island-architecture#entra-el-streaming-html","content":"HTTP/1.1 soporta chunked transfer encoding desde 1999. La idea es simple: en vez de buffear toda la respuesta, mandá chunks a medida que estén disponibles.\n\n```javascript\n// Node.js streaming response - lo básico\nimport { Readable } from 'stream';\n\napp.get('/product/:id', async (req, res) => {\n res.setHeader('Content-Type', 'text/html');\n res.setHeader('Transfer-Encoding', 'chunked');\n\n // Mandá el shell inmediatamente\n res.write(`\n <!DOCTYPE html>\n <html>\n <head><title>Producto</title><link rel=\"stylesheet\" href=\"/styles.css\"></head>\n <body>\n <header><!-- el nav renderiza inmediatamente --></header>\n <main>\n `);\n\n // Los detalles del producto vienen rápido - mandalos\n const product = await fetchProduct(req.params.id); // ~180ms\n res.write(`\n <h1>${product.name}</h1>\n <p class=\"price\">${product.price}</p>\n <img src=\"${product.image}\" alt=\"${product.name}\">\n <div id=\"reviews-placeholder\">Cargando reviews...</div>\n <div id=\"recommendations-placeholder\">Cargando recomendaciones...</div>\n `);\n\n // Disparamos los requests lentos en paralelo\n const [reviews, recommendations] = await Promise.all([\n fetchReviews(req.params.id), // ~600ms\n fetchRecommendations(req.params.id) // ~1200ms\n ]);\n\n res.write(`\n <script>\n document.getElementById('reviews-placeholder').outerHTML =\n '${renderReviews(reviews)}';\n document.getElementById('recommendations-placeholder').outerHTML =\n '${renderRecommendations(recommendations)}';\n </script>\n `);\n\n res.end('</main></body></html>');\n});\n```\n\nEsto es rudimentario pero ilustrativo. El browser recibe el header y la info del producto en 200ms y puede empezar a renderizar. Las reviews y recomendaciones gotean después. Pero hay un problema: seguimos esperando las reviews antes de mandar las recomendaciones, aunque las recomendaciones podrían terminar primero."},{"heading":"Out-of-Order Streaming","level":2,"url":"https://lucioduran.com/es/blog/streaming-html-out-of-order-island-architecture#out-of-order-streaming","content":"Acá resulta interesante. El out-of-order streaming desacopla el orden de resolución de datos del orden de aparición en el DOM. El servidor manda elementos placeholder, y después los reemplaza con contenido real a medida que cada fuente de datos se resuelve, en el orden que sea que terminen.\n\nMarko viene haciendo esto desde 2019. El modelo mental es así:\n\n```html\n<!-- Lo que el servidor manda PRIMERO (inmediatamente) -->\n<div id=\"product-shell\">\n <h1>Cargando...</h1>\n</div>\n<div id=\"reviews-slot\" hidden>\n <!-- Placeholder, se va a swappear -->\n</div>\n<div id=\"recs-slot\" hidden>\n <!-- Placeholder, se va a swappear -->\n</div>\n\n<!-- Script inline para reemplazo out-of-order -->\n<script>\n window.$RS = function(slotId, html) {\n const slot = document.getElementById(slotId);\n const template = document.createElement('template');\n template.innerHTML = html;\n slot.replaceWith(template.content);\n };\n</script>\n\n<!-- Lo que el servidor manda cuando los datos del producto se resuelven (~180ms) -->\n<script>$RS('product-shell', '<h1>Nike Air Max 90</h1><p>$129.99</p>')</script>\n\n<!-- Lo que el servidor manda cuando las recomendaciones se resuelven (~800ms) -->\n<!-- Ojo: esto llega ANTES que las reviews aunque está después en el DOM -->\n<script>$RS('recs-slot', '<div class=\"recs\">...</div>')</script>\n\n<!-- Lo que el servidor manda cuando las reviews finalmente se resuelven (~1200ms) -->\n<script>$RS('reviews-slot', '<div class=\"reviews\">...</div>')</script>\n```\n\nLo hermoso es que el browser procesa cada chunk a medida que llega. El usuario ve el producto casi al instante, después aparecen las recomendaciones, después las reviews. No se necesita un framework de JavaScript para este render inicial, solo scripts inline y manipulación del DOM."},{"heading":"La Implementación de Marko","level":3,"url":"https://lucioduran.com/es/blog/streaming-html-out-of-order-island-architecture#la-implementacin-de-marko","content":"El approach de Marko al out-of-order streaming es el más maduro. Usa un tag ` <await> ` que crea streaming boundaries:\n\n```marko\n// product-page.marko\n<layout>\n <@body>\n <await(productPromise)>\n <@then|product|>\n <product-details product=product />\n </@then>\n <@placeholder>\n <product-skeleton />\n </@placeholder>\n </await>\n\n <await(reviewsPromise) client-reorder>\n <@then|reviews|>\n <review-list reviews=reviews />\n </@then>\n <@placeholder>\n <reviews-skeleton />\n </@placeholder>\n </await>\n\n <await(recommendationsPromise) client-reorder>\n <@then|recs|>\n <recommendation-carousel items=recs />\n </@then>\n <@placeholder>\n <recs-skeleton />\n </@placeholder>\n </await>\n </@body>\n</layout>\n```\n\nEl atributo ` client-reorder ` es la clave. Sin él, Marko streamea en orden del documento (esperando a los await blocks anteriores antes de los posteriores). Con él, cada bloque se resuelve independientemente, y Marko inyecta un runtime diminuto (~1KB) para manejar los DOM swaps."},{"heading":"El Approach de React con Suspense","level":3,"url":"https://lucioduran.com/es/blog/streaming-html-out-of-order-island-architecture#el-approach-de-react-con-suspense","content":"React 18+ soporta streaming SSR con Suspense boundaries:\n\n```jsx\n// ProductPage.jsx\nimport { Suspense } from 'react';\nimport { renderToPipeableStream } from 'react-dom/server';\n\nfunction ProductPage({ productId }) {\n return (\n <Layout>\n <Suspense fallback={<ProductSkeleton />}>\n <ProductDetails id={productId} />\n </Suspense>\n\n <Suspense fallback={<ReviewsSkeleton />}>\n <ReviewList id={productId} />\n </Suspense>\n\n <Suspense fallback={<RecommendationsSkeleton />}>\n <Recommendations id={productId} />\n </Suspense>\n </Layout>\n );\n}\n\n// Server entry\napp.get('/product/:id', (req, res) => {\n const { pipe } = renderToPipeableStream(\n <ProductPage productId={req.params.id} />,\n {\n bootstrapScripts: ['/client.js'],\n onShellReady() {\n res.setHeader('Content-Type', 'text/html');\n pipe(res);\n },\n onError(error) {\n console.error(error);\n res.statusCode = 500;\n }\n }\n );\n});\n```\n\nEl streaming de React funciona, pero viene con un costo: teners que hidratar el árbol de componentes entero. Aunque solo dos componentes sean interactivos, React descarga y re-ejecuta todo. Acá es donde entra la island architecture."},{"heading":"Island Architecture: Interactividad Selectiva","level":2,"url":"https://lucioduran.com/es/blog/streaming-html-out-of-order-island-architecture#island-architecture-interactividad-selectiva","content":"El término \"island architecture\" viene del blog post de Jason Miller de 2020, aunque el concepto existía de antes. La idea: una página es un mar de HTML estático con islas de interactividad. Solo las islas envían JavaScript."},{"heading":"La Implementación de Astro","level":3,"url":"https://lucioduran.com/es/blog/streaming-html-out-of-order-island-architecture#la-implementacin-de-astro","content":"[Astro](<https://astro.build/>) es el referente de island architecture:\n\n```astro\n---\n// product-page.astro\nimport Layout from '../layouts/Layout.astro';\nimport ProductDetails from '../components/ProductDetails.astro'; // ¡Estático!\nimport ReviewList from '../components/ReviewList.astro'; // ¡Estático!\nimport AddToCart from '../components/AddToCart.tsx'; // ¡Isla!\nimport ImageCarousel from '../components/ImageCarousel.svelte'; // ¡Isla!\n\nconst product = await fetchProduct(Astro.params.id);\nconst reviews = await fetchReviews(Astro.params.id);\n---\n\n<Layout title={product.name}>\n <!-- HTML estático, cero JS -->\n <ProductDetails product={product} />\n\n <!-- Isla interactiva: carga cuando es visible en el viewport -->\n <ImageCarousel client:visible images={product.images} />\n\n <!-- Isla interactiva: carga inmediatamente (above the fold, crítico) -->\n <AddToCart client:load productId={product.id} price={product.price} />\n\n <!-- HTML estático, cero JS -->\n <ReviewList reviews={reviews} />\n</Layout>\n```\n\nLas directivas ` client:* ` controlan cuándo carga el JavaScript de cada isla:\n\n```typescript\n// Estrategias de carga de islas en Astro\nclient:load // Carga e hidrata inmediatamente al cargar la página\nclient:idle // Carga después de la carga inicial (requestIdleCallback)\nclient:visible // Carga cuando el componente entra al viewport (IntersectionObserver)\nclient:media // Carga cuando se cumple un CSS media query\nclient:only // Saltea SSR, solo render en cliente\n```"},{"heading":"Qwik: Más Allá de las Islas","level":3,"url":"https://lucioduran.com/es/blog/streaming-html-out-of-order-island-architecture#qwik-ms-all-de-las-islas","content":"[Qwik](<https://qwik.dev/>) toma un enfoque fundamentalmente diferente. En vez de hidratación (re-ejecutar código de componentes en el cliente), Qwik usa resumabilidad. El servidor serializa el estado de los componentes y los bindings de eventos en el HTML, y el cliente literalmente retoma de donde dejó el servidor.\n\n```tsx\n// product-page.tsx (Qwik)\nimport { component$, useSignal, $ } from '@builder.io/qwik';\nimport { routeLoader$ } from '@builder.io/qwik-city';\n\nexport const useProductData = routeLoader$(async ({ params }) => {\n const [product, reviews] = await Promise.all([\n fetchProduct(params.id),\n fetchReviews(params.id)\n ]);\n return { product, reviews };\n});\n\nexport default component$(() => {\n const data = useProductData();\n const quantity = useSignal(1);\n\n // Este handler se carga lazy SOLO cuando el usuario clickea\n const addToCart = $(() => {\n cart.add(data.value.product.id, quantity.value);\n });\n\n return (\n <div>\n <h1>{data.value.product.name}</h1>\n <p class=\"price\">${data.value.product.price}</p>\n\n <div class=\"quantity-selector\">\n <button onClick$={() => quantity.value = Math.max(1, quantity.value - 1)}>-</button>\n <span>{quantity.value}</span>\n <button onClick$={() => quantity.value++}>+</button>\n </div>\n\n <button onClick$={addToCart}>Agregar al Carrito</button>\n\n {data.value.reviews.map(review => (\n <div key={review.id} class=\"review\">\n <p>{review.text}</p>\n <span>{'★'.repeat(review.rating)}</span>\n </div>\n ))}\n </div>\n );\n});\n```\n\nEl marcador ` $() ` le dice al optimizador de Qwik dónde crear boundaries de lazy-loading. Cada ` $ ` se convierte en un chunk separado que solo se descarga cuando se dispara. La carga inicial de la página envía prácticamente cero JavaScript. El click handler de \"Agregar al Carrito\" se fetchea de la red solo cuando el usuario clickea el botón.\n\nObservar cómo se ve el HTML output de Qwik:\n\n```html\n<button on:click=\"product-page_component_addtocart_0lz8qwik.js#s_addToCart\"\n q:id=\"3\">\n Agregar al Carrito\n</button>\n```\n\nEse atributo ` on:click ` contiene una URL al click handler. Cuando el usuario clickea, el runtime diminuto (~1KB) de Qwik fetchea ese script, deserializa el estado y ejecuta el handler. Sin hidratación. Sin re-renderizar todo el árbol. Solo el handler."},{"heading":"Comparación de Performance: Números Reales","level":2,"url":"https://lucioduran.com/es/blog/streaming-html-out-of-order-island-architecture#comparacin-de-performance-nmeros-reales","content":"Benchmarkeé la misma página de producto con tres enfoques en un dataset real de e-commerce. Mismo servidor (AWS t3.medium), mismas latencias de API, mismo contenido:\n\n```\n┌────────────────────┬─────────┬───────────┬──────────────┬───────────┐\n│ Métrica │ React │ Astro │ Qwik │ Marko │\n│ │ SSR │ Islands │ Resumable │ Streaming │\n├────────────────────┼─────────┼───────────┼──────────────┼───────────┤\n│ TTFB │ 1820ms │ 420ms │ 390ms │ 180ms │\n│ FCP │ 2100ms │ 520ms │ 480ms │ 240ms │\n│ LCP │ 2400ms │ 680ms │ 590ms │ 520ms │\n│ TTI │ 3800ms │ 710ms │ 490ms* │ 1200ms │\n│ JS transferido │ 187KB │ 24KB │ 2KB inicial │ 42KB │\n│ Total blocking │ 340ms │ 45ms │ 0ms │ 120ms │\n└────────────────────┴─────────┴───────────┴──────────────┴───────────┘\n```\n\nMarko gana en TTFB porque su out-of-order streaming manda el primer byte casi inmediatamente. Qwik gana en TTI porque efectivamente no hay costo de hidratación. Astro queda en un punto óptimo para páginas heavy en contenido. React SSR sin streaming es el baseline que todos tratan de superar."},{"heading":"Cuándo Usar Qué","level":2,"url":"https://lucioduran.com/es/blog/streaming-html-out-of-order-island-architecture#cundo-usar-qu","content":"Después de construir sitios en producción con los tres enfoques, aquí va mi evaluación honesta:\n\n**Usar Astro** cuando tu sitio es primariamente contenido con toques de interactividad. Blogs, documentación, sitios de marketing, portfolios. El modelo de islas mapea perfecto a estos case es de uso.\n\n**Usar Qwik** cuando estés construyendo una aplicación completa, dashboards, e-commerce, productos SaaS, donde cada componente es potencialmente interactivo, pero se desea performance nivel MPA. La curva de aprendizaje de la sintaxis ` $ ` es real, y el ecosistema es más chico, pero las características de performance son notables.\n\n**Usar Marko** si se está a escala eBay y se necesita la implementación de streaming más battle-tested. Marko viene streameando HTML en producción hace años en eBay, manejando millones de requests. El ecosistema afuera de eBay es chico, que es su mayor limitación.\n\n**Usar React con streaming** si tu equipo ya sabe React y no es posible justificar una migración de framework. El streaming SSR de React con Suspense es una mejora masiva sobre el enfoque con buffer, aunque no matchee la eficiencia cruda de las alternativas.\n\nLa industria está convergiendo en un modelo donde el servidor hace la mayor parte del trabajo de rendering, y el cliente solo maneja interactividad genuina. Ya sea que lo llamen islas, resumabilidad o hidratación selectiva, la dirección es clara. La era de enviar 300KB de JavaScript para renderizar una tarjeta de producto se está terminando, y es un cambio bienvenido."},{"heading":"Preguntas frecuentes","level":2,"url":"https://lucioduran.com/es/blog/streaming-html-out-of-order-island-architecture#questions","content":""},{"heading":"¿Qué es el out-of-order streaming?","level":3,"url":"https://lucioduran.com/es/blog/streaming-html-out-of-order-island-architecture#question-1","content":"Es una técnica donde el servidor envía chunks de HTML a medida que están listos, sin importar su posición en el DOM. Se renderiza un placeholder primero, que después se reemplaza con contenido real via scripts inline cuando los datos se resuelven. Marko fue pionero en este approach."},{"heading":"¿En qué se diferencia island architecture de partial hydration?","level":3,"url":"https://lucioduran.com/es/blog/streaming-html-out-of-order-island-architecture#question-2","content":"Island architecture trata los componentes interactivos como islas aisladas en un mar de HTML estático. Partial hydration es el mecanismo que hace funcionar las islas, solo el código de la isla se descarga y ejecuta, no el JavaScript de toda la página."},{"heading":"¿Debería usar Marko, Qwik o Astro para un proyecto nuevo?","level":3,"url":"https://lucioduran.com/es/blog/streaming-html-out-of-order-island-architecture#question-3","content":"Astro para sitios heavy en contenido con toques de interactividad. Qwik para aplicaciones completas donde se desea feel de SPA con performance de MPA. Marko si se está en el ecosistema de eBay o se desea la implementación de streaming más madura."},{"heading":"Tips","level":2,"url":"https://lucioduran.com/es/blog/streaming-html-out-of-order-island-architecture#article-tips","content":"1. **Tip:** El out-of-order streaming solo ayuda si tu shell renderiza rápido. Colocar los ` <Suspense> ` boundaries alrededor de data fetches lentos, no alrededor de componentes de layout.\n2. **Tip:** Con islas en Astro, utilizar ` client:visible ` para componentes interactivos below-the-fold y ` client:idle ` para UI no crítica como widgets de analytics.\n3. **Tip:** La resumabilidad de Qwik elimina la hidratación por completo, pero solo si evitás ` useVisibleTask$ ` que fuerza ejecución eager. Tratalo como escape hatch, no como default.\n4. **Tip:** Probar streaming con ` curl --no-buffer ` para ver realmente los chunks llegando. El DevTools del browser comprime el stream y oculta el comportamiento real.\n\n- streaming-html\n- island-architecture\n- progressive-hydration\n- marko\n- qwik\n- astro\n- web-performance\n\n[Volver al Blog](<https://lucioduran.com/es/blog>)"}],"links":[{"text":"English","url":"https://lucioduran.com/blog/streaming-html-out-of-order-island-architecture"},{"text":"Astro","url":"https://astro.build/"},{"text":"Qwik","url":"https://qwik.dev/"},{"text":"Volver al Blog","url":"https://lucioduran.com/es/blog"}],"structuredData":[{"@context":"https://schema.org","@graph":[{"@type":"BlogPosting","@id":"https://lucioduran.com/es/blog/streaming-html-out-of-order-island-architecture#article","mainEntityOfPage":{"@type":"WebPage","@id":"https://lucioduran.com/es/blog/streaming-html-out-of-order-island-architecture"},"headline":"Streaming HTML: Out-of-Order Streaming, Island Architecture e Hidratación Selectiva","description":"Un análisis de estrategias de streaming HTML con Marko, Qwik y Astro, out-of-order streaming, los internals de island architecture, y por qué la hidratación progresiva cambia todo en performance web.","datePublished":"2026-01-03","author":{"@type":"Person","@id":"https://lucioduran.com/#person","name":"Lucio Durán","url":"https://lucioduran.com"},"publisher":{"@type":"Person","@id":"https://lucioduran.com/#person","name":"Lucio Durán","url":"https://lucioduran.com"},"image":"https://statics.lucioduran.com/images/lucioduran-social-card.jpg","url":"https://lucioduran.com/es/blog/streaming-html-out-of-order-island-architecture","inLanguage":"es","isPartOf":{"@id":"https://lucioduran.com/blog#blog"},"articleSection":"Frontend","keywords":"streaming-html, island-architecture, progressive-hydration, marko, qwik, astro, web-performance","wordCount":1685,"timeRequired":"PT9M","workTranslation":[{"@type":"BlogPosting","inLanguage":"en","url":"https://lucioduran.com/blog/streaming-html-out-of-order-island-architecture"}]},{"@type":"BreadcrumbList","itemListElement":[{"@type":"ListItem","position":1,"name":"Inicio","item":"https://lucioduran.com/"},{"@type":"ListItem","position":2,"name":"Blog","item":"https://lucioduran.com/es/blog"},{"@type":"ListItem","position":3,"name":"Streaming HTML: Out-of-Order Streaming, Island Architecture e Hidratación Selectiva","item":"https://lucioduran.com/es/blog/streaming-html-out-of-order-island-architecture"}]},{"@type":"FAQPage","mainEntity":[{"@type":"Question","name":"¿Qué es el out-of-order streaming?","acceptedAnswer":{"@type":"Answer","text":"Es una técnica donde el servidor envía chunks de HTML a medida que están listos, sin importar su posición en el DOM. Se renderiza un placeholder primero, que después se reemplaza con contenido real via scripts inline cuando los datos se resuelven. Marko fue pionero en este approach."}},{"@type":"Question","name":"¿En qué se diferencia island architecture de partial hydration?","acceptedAnswer":{"@type":"Answer","text":"Island architecture trata los componentes interactivos como islas aisladas en un mar de HTML estático. Partial hydration es el mecanismo que hace funcionar las islas, solo el código de la isla se descarga y ejecuta, no el JavaScript de toda la página."}},{"@type":"Question","name":"¿Debería usar Marko, Qwik o Astro para un proyecto nuevo?","acceptedAnswer":{"@type":"Answer","text":"Astro para sitios heavy en contenido con toques de interactividad. Qwik para aplicaciones completas donde se desea feel de SPA con performance de MPA. Marko si se está en el ecosistema de eBay o se desea la implementación de streaming más madura."}}]}]}],"contentHash":"9b542c2727d7e9ef4171b3acd3e4d3dbc78d5af4975ef325caf6a629eebc67bc","provenance":{"method":"Deterministic serialization of the server-rendered main element","source":"https://lucioduran.com/es/blog/streaming-html-out-of-order-island-architecture","hashAlgorithm":"sha256","hashScope":"JSON document fields before contentHash and provenance","sourceHTMLBytes":74971,"contentBytes":16001}}
