wandres.dev
HIDRATACIÓN · El impuesto del SSR

Arquitectura de islas: JavaScript como excepción

Qué es exactamente una isla, cómo se comunican entre sí sin volver a montar una aplicación entera, el truco del slot para no arrastrar hijos, y cuándo las islas estorban.

⏱ 19 min

La arquitectura de islas invierte el valor por defecto. En una aplicación normal, todo es JavaScript y algunas partes se optimizan para no serlo; en una arquitectura de islas, todo es HTML y algunas partes se declaran interactivas. El cambio parece cosmético y no lo es: cambia quién tiene que justificarse. Y por eso su efecto sobre el peso de una web es mucho mayor de lo que sugiere la técnica en sí.

🎯 Al terminar esta lección sabrás
  • Definir una isla por sus tres propiedades: independencia, frontera de serialización y disparador propio.
  • Resolver la comunicación entre islas sin reintroducir una aplicación global.
  • Usar contenido en ranuras para que los hijos de una isla no entren en su paquete.
  • Reconocer los productos en los que las islas son la arquitectura equivocada.

Qué es exactamente una isla

Una isla es un componente interactivo dentro de una página que, por lo demás, es HTML estático. Tres propiedades la definen.

Es independiente. No hay una raíz común que las coordine. Cada isla se monta por su cuenta, tiene su propio ciclo de vida y no sabe que las demás existen. Si una falla al montarse, las otras siguen funcionando: no hay un árbol que se caiga entero.

Tiene una frontera de serialización. Lo que entra en la isla como props viaja en el HTML como datos. Lo que se queda fuera nunca llega al navegador.

Tiene su propio disparador. Cada isla decide cuándo se hidrata, con los criterios de la hidratación progresiva. Una puede cargar inmediatamente y otra esperar a ser visible.

La independencia es la propiedad interesante y la que más consecuencias tiene. En una aplicación con una raíz única, el fallo de un componente al montar puede tumbar el árbol; en una página con islas, el peor caso de un fallo es que un widget no funcione mientras el resto de la página sigue perfectamente. Es una propiedad de robustez que se disfruta especialmente cuando la isla que falla es la de un tercero.

Comunicación entre islas

Es la primera pregunta que aparece en cuanto sales del ejemplo de una sola isla: el botón de añadir al carrito está en una isla y el contador de la cabecera está en otra. ¿Cómo se hablan?

Hay tres respuestas, en orden de preferencia.

Un módulo compartido. Las dos islas importan el mismo módulo, que mantiene el estado y una lista de suscriptores. Es la solución más simple y no necesita nada especial.

// tienda-carrito.js
const suscriptores = new Set();
let estado = { unidades: 0, total: 0 };

export function leer() {
  return estado;
}

export function escribir(parcial) {
  estado = { ...estado, ...parcial };
  for (const notificar of suscriptores) notificar(estado);
}

export function suscribir(notificar) {
  suscriptores.add(notificar);
  notificar(estado);                       // valor inicial inmediato
  return () => suscriptores.delete(notificar);
}

Con un aviso que hay que verificar y que produce el bug más desconcertante de esta arquitectura: solo funciona si el empaquetador emite el módulo compartido en un único trozo. Si cada isla se compila de forma independiente, cada una recibe su propia copia del módulo con su propio estado, y las dos islas viven en universos separados sin que nada falle visiblemente. Compruébalo mirando la salida del build: el módulo debe aparecer una vez, en un trozo común importado por ambas.

Eventos personalizados en el documento. Inmune al problema anterior porque el canal es el DOM y no el grafo de módulos.

// Isla emisora
document.dispatchEvent(
  new CustomEvent('carrito:cambio', { detail: { unidades: 3 } })
);

// Isla receptora
document.addEventListener('carrito:cambio', (e) => {
  pintarContador(e.detail.unidades);
}, { signal });

Más verboso y sin tipos, pero robusto y depurable: puedes escuchar el evento desde la consola y ver todo el tráfico entre islas.

El estado en la URL o en el propio DOM. Para lo que el usuario espera que sobreviva a una recarga o a compartir el enlace —filtros, pestaña activa, orden de una tabla— el sitio correcto es la URL, y las islas la leen y la escriben. Para estado que solo describe la página, un atributo de datos en un contenedor común funciona y tiene la ventaja de ser inspeccionable.

Lo que no hay que hacer es reintroducir un contenedor global de estado que todas las islas importan y que acaba conociendo el modelo entero de la aplicación. En cuanto eso pasa, has vuelto a tener una aplicación de página única con pasos extra, y todas las islas necesitan cargarse a la vez para que el estado sea coherente.

Anidar islas y el truco de las ranuras

Un malentendido frecuente: si un componente interactivo envuelve mucho contenido, parece que todo ese contenido tiene que entrar en la isla. No es así, y la técnica que lo evita es la más rentable de esta lección.

El contenido que se pasa como hijo a través de una ranura lo renderiza el servidor, y viaja como HTML dentro de la isla sin formar parte de su paquete.

---
import Acordeon from '../componentes/Acordeon.jsx';
const articulo = await cargarArticulo();
---
<Acordeon client:visible titulo={articulo.titulo}>
  <!-- Renderizado en el servidor. NO entra en el paquete del acordeon:
       viaja como HTML y el acordeon solo lo muestra u oculta. -->
  <article set:html={articulo.cuerpo}></article>
</Acordeon>

El Acordeon solo necesita saber abrir y cerrar; su JavaScript son doscientos bytes. El artículo, que puede ser cincuenta kilobytes de marcado con imágenes y tablas, no pasa por el empaquetador ni por el framework de cliente.

La regla que se deduce: cuando un componente interactivo envuelva contenido, ese contenido va por ranura y no por props. Pasarlo por props significa serializarlo y renderizarlo en el cliente; pasarlo por ranura significa mandarlo como HTML. La diferencia en una página con contenido es de un orden de magnitud.

Complementariamente, existen las islas de servidor: huecos que se renderizan en el servidor bajo demanda y llegan por streaming, para lo dinámico o personalizado dentro de una página cacheada. Son el otro eje del árbol de decisión del nivel anterior, y encajan de forma natural con las islas de cliente porque el mecanismo de sustitución es el mismo.

Cuándo las islas son la arquitectura equivocada

Hay que decirlo con la misma claridad que las ventajas.

Cuando casi todo es interactivo. Un editor de documentos, una hoja de cálculo, un panel con arrastre y soltar, una herramienta de diseño. Si el ochenta por ciento de la interfaz responde a la entrada del usuario, las islas no reducen nada y añaden fricción: fronteras de serialización donde no hacen falta, comunicación indirecta entre partes que deberían compartir estado directamente, y un modelo mental partido en dos.

Cuando el estado es transversal y cambia constantemente. Si mover un elemento actualiza a la vez seis zonas de la pantalla, el paso de mensajes entre islas es una reimplementación peor de lo que un árbol reactivo hace de forma natural.

Cuando la primera carga no importa. Una aplicación interna que el usuario abre por la mañana y deja abierta ocho horas. Ahí el tiempo de arranque es irrelevante y lo que importa es la latencia de las operaciones, que las islas no mejoran.

El criterio numérico, aproximado pero útil: estima qué fracción de los nodos de tu página pertenece a componentes que necesitan estado o manejadores. Por debajo de un veinte o treinta por ciento, las islas son claramente la mejor opción. Por encima del setenta, son un estorbo. En medio, depende de si esa interactividad está concentrada en zonas —bien para islas— o repartida por toda la pantalla —mal—.

Y una observación que ahorra discusiones: un producto puede ser las dos cosas. El sitio público con islas y la aplicación tras el acceso como aplicación completa, con despliegues y equipos distintos. Es una separación limpia porque los dos tienen requisitos genuinamente distintos, y forzarlos a compartir arquitectura solo sirve para que uno de los dos vaya mal.

Lo que cambia una arquitectura de islas no son los bytes: es quién tiene que justificarse

El efecto de las islas sobre el peso de un sitio es mucho mayor de lo que la técnica explica por sí sola, y la razón no es técnica. En una aplicación construida sobre un framework de cliente, añadir un componente interactivo es lo natural: no hay que pedir permiso, no hay que rellenar nada, el coste es invisible porque se reparte en un paquete que ya existía. Quitar interactividad, en cambio, requiere un esfuerzo consciente y a menudo una discusión. El resultado agregado de miles de decisiones individuales bajo ese régimen es predecible y se observa en cualquier código de más de dos años: el peso crece de forma monótona, y nadie es responsable porque cada incremento era razonable. En una arquitectura de islas la asimetría se invierte. El valor por defecto de un componente es cero JavaScript, y hacer que un componente sea interactivo exige un acto explícito: marcarlo, elegir su disparador, decidir qué props cruzan la frontera. Ese acto es visible en el diff, tiene un nombre concreto, y cualquiera que lo lea puede preguntar “¿esto necesita ser interactivo?”. No es una pregunta que salga en una revisión de código en la otra arquitectura, porque allí no hay nada que la provoque. La técnica no impide nada; lo que hace es mover el coste al sitio donde se ve. Y ese es un principio de diseño de sistemas que va mucho más allá del frontend, porque describe la única forma que funciona de gobernar una propiedad que se degrada por acumulación de decisiones pequeñas y locales. No puedes gobernarla con reglas, porque cada decisión individual las cumple. No puedes gobernarla con revisiones, porque cada diff es defendible. Solo puedes gobernarla cambiando el valor por defecto, de modo que la dirección que quieres sea la que no requiere justificación. Es la misma razón por la que un lenguaje con inmutabilidad por defecto produce código distinto de uno con mutabilidad por defecto teniendo ambos las dos opciones, y por la que un presupuesto que falla el build funciona y uno que manda un correo no. Los valores por defecto no son una preferencia: son política.

⚔️ Convierte una página a islas
  1. Coge tu página de contenido más pesada y marca qué componentes necesitan estado. Calcula la fracción sobre el total de nodos.
  2. Conviértela a islas y compara el JavaScript enviado antes y después.
  3. Aplica el truco de la ranura a un componente que envuelva contenido y mide cuánto marcado sale del paquete.
  4. Implementa la comunicación entre dos islas con un módulo compartido y verifica en la salida del build que el módulo aparece una sola vez.
  5. Estima la fracción interactiva de tu aplicación tras el acceso y decide, con ese número, si allí también corresponden islas.