Compartir estado entre islas y el presupuesto de JS
Las islas son raíces aisladas y no comparten estado por sí solas: para comunicarlas se usa un almacén externo diminuto como nanostores, un átomo que ambas importan y que el sistema de módulos convierte en singleton. Cerrar el nivel exige además mirar de frente el coste real de hidratar —runtime del framework, código, deserialización, CPU— y adoptar un presupuesto de JavaScript que gobierne cuántas islas y de qué peso te puedes permitir.
Por qué las islas no comparten estado
En una SPA, un proveedor de contexto en la raíz alcanza a cualquier componente del árbol, porque todos cuelgan de la misma raíz. En Astro no existe esa raíz: cada isla es su propia aplicación, montada por separado, con su propio ciclo de vida. Un contexto de React declarado en una isla no llega a otra isla; ni siquiera se conocen. Las props que el servidor les pasó se serializaron una vez y quedaron congeladas dentro de cada una.
Esa independencia es la fuente del rendimiento —lo viste en el nivel 1— pero impone una regla: la comunicación entre islas ocurre fuera de los frameworks, en un terreno neutral que todas puedan compartir. Ese terreno puede ser el objeto window con eventos personalizados, o —mucho mejor para estado— un almacén diseñado para ser importado desde cualquier isla, del framework que sea.
Hay una tercera vía, a veces la más simple: si dos piezas interactivas están muy acopladas, súbelas a una única isla que las englobe. Dentro de ella vuelves a tener un árbol común y todo el estado local que quieras. El coste es que esa isla es mayor y se hidrata como una sola unidad; la ganancia, que no necesitas ningún mecanismo externo. Almacén compartido, eventos o una isla que lo abarque: son tres respuestas al mismo problema, ordenadas de más desacoplado a más simple.
nanostores: un átomo compartido
nanostores es una librería de estado diminuta —cientos de bytes— y agnóstica de framework. Su unidad básica es el átomo: un valor observable que se declara en un módulo aparte y se importa donde haga falta. Defines el estado una vez, en un .ts neutral, y cada isla lo importa.
// stores/carrito.tsimport { atom } from 'nanostores';export const total = atom(0);export function agregar() { total.set(total.get() + 1);}Cada framework tiene un enlace ligero para suscribirse. En React es useStore, que vuelve a renderizar el componente cuando el átomo cambia; Vue, Solid y Preact tienen el suyo, y Svelte consume el átomo directamente con su contrato de store.
import { useStore } from '@nanostores/react';import { total, agregar } from '../stores/carrito';export default function Contador() { const valor = useStore(total); return <button onClick={agregar}>Carrito: {valor}</button>;}La clave de por qué esto funciona entre islas está en el sistema de módulos. Cuando dos islas importan stores/carrito.ts, el navegador carga ese módulo una sola vez y ambas reciben la misma instancia del átomo: los módulos ESM son singletons. Por eso un cambio hecho desde una isla se refleja en la otra al instante, aunque estén escritas en frameworks distintos. El átomo es el punto de encuentro que ninguna de las dos posee y que las dos comparten.
El ecosistema añade dos piezas que usarás pronto: computed, que deriva un átomo a partir de otros, y @nanostores/persistent, que respalda un átomo en localStorage para que sobreviva a las recargas —un tema, un carrito— sin cambiar la forma de consumirlo.
import { computed } from 'nanostores';import { persistentAtom } from '@nanostores/persistent';import { total } from './carrito';export const tema = persistentAtom('tema', 'claro');export const vacio = computed(total, (n) => n === 0);El coste real de hidratar
Cerrar el nivel exige nombrar con precisión lo que cuesta una isla, porque solo así se decide bien. Hidratar no es gratis, y su factura tiene cuatro líneas:
- El runtime del framework, descargado una vez y compartido entre las islas de ese framework.
- El código del componente y de sus dependencias.
- La deserialización de las props que viajaron en el HTML.
- El tiempo de CPU de la hidratación, que recorre el DOM y engancha escuchadores.
De esas cuatro líneas, la que más varía está en tu mano: el peso del componente y de sus dependencias. Una isla que importa una librería de fechas de doscientos kilobytes para formatear una hora cuesta eso, se vea o no. Por eso la elección de framework importa —Preact, Solid o Svelte pesan una fracción de React— y por eso una dependencia pesada dentro de una isla es más cara que la isla misma.
El presupuesto de JavaScript
La herramienta mental que ordena todo esto es un presupuesto de JavaScript: una cifra máxima de kilobytes de JS que te comprometes a no rebasar en una página, fijada de antemano y vigilada en cada cambio. Un presupuesto convierte una intuición vaga —“no mandes mucho JS”— en una restricción concreta que obliga a elegir, porque cada isla nueva consume de una bolsa finita.
Medir, no adivinar
Un presupuesto solo sirve si lo mides. El propio build de Astro informa del peso de cada trozo de cliente que emite, y las herramientas de análisis de bundle desglosan qué dependencia pesa dentro de cada isla. La rutina sana es revisar ese desglose cuando añades una isla o una librería: si un formateador de fechas mete cien kilobytes, lo ves antes de enviarlo, no en las métricas de campo semanas después. Adivinar el peso es como adivinar el saldo de una cuenta; el presupuesto se administra con el extracto delante. Los recortes más rentables, en orden habitual de impacto:
- Cambiar una isla
client:loadporclient:visibleoclient:idle. - Sustituir una dependencia pesada por una nativa o más ligera.
- Fusionar varias islas diminutas en una para no repetir enganches.
- Convertir en server island lo que solo muestra datos sin interacción.
- Cuestionar si la isla debe existir: a veces HTML y CSS bastan.
Con esa cifra en la cabeza, las decisiones del nivel se ordenan solas. Antes de crear una isla, preguntas si hace falta interactividad o si un elemento nativo y algo de CSS resuelven el caso sin coste. Si hace falta, eliges el framework más ligero que sirva y la directiva que difiera o condicione su carga. Reservas client:load para lo crítico y client:only para lo que exige el navegador. Y para lo que solo es dinámico, recurres a una server island antes que a una isla de cliente. Cada decisión es un cargo contra el presupuesto, y el presupuesto es lo que impide que la suma se descontrole.