Islas Resumibles en Bun: Lecciones de diseño del marco de gres cerámico
Cómo un framework orientado al servidor trata el HTML como predeterminado, delimita JavaScript en islas reanudables y gestiona la exportación estática, los errores, CSP y el despliegue.
La mayoría de las páginas en la web se conforman con unos pocos controles interactivos dispersos sobre su contenido, pero el modelo de desarrollo predominante envía toda la página al navegador como una aplicación JavaScript. Stoneware, un framework joven de código abierto basado en TypeScript y construido sobre Bun, parte de una premisa opuesta: el HTML es lo que el navegador recibe por defecto, y un componente debe optar explícitamente antes de que se envíe cualquier JavaScript para él. Analizar su diseño es una forma útil de comprender los conceptos de “islas”, la posibilidad de reutilización, la exportación estática y las partes menos visibles de la ingeniería de frameworks, como los mensajes de error, el ordenamiento de seguridad y el empaquetado para despliegue. Al final, deberías poder determinar cuándo una arquitectura basada en servidores y “islas” es adecuada para tu proyecto, así como qué problemas evitar si decides crearla o adoptarla.
Por qué una página de contenido no debería convertirse en una aplicación
Imagínese una página de producto típica. Tiene un título, una foto, una descripción, una lista de especificaciones, un precio, productos relacionados, reseñas y navegación del sitio. De todo eso, quizás solo dos elementos realmente responden al usuario: el menú móvil y el botón “Añadir al carrito”. Todo lo demás es contenido estático que el servidor ya sabe cómo generar.
Un enfoque basado en la renderización por el cliente o en la hidratación total sigue pidiendo al navegador que descargue, analice y ejecute el código que representa toda la página, solo para que esos dos controles funcionen. La pregunta del enfoque centrado en el servidor es sencilla: ¿qué pasaría si el navegador solo recibiera código para las partes que lo necesitan?
El modelo mental: HTML más islas
La arquitectura divide una página en dos tipos de áreas. La mayor parte es HTML generado por el servidor. Las partes interactivas se convierten en “islas”, pequeñas regiones autónomas que contienen su propio JavaScript. Ambas terminan en el mismo documento del navegador.
Web page
│
┌───────────┴───────────┐
│ │
HTML Islands
│ │
Server rendered JavaScript
│ │
└───────────┬───────────┘
│
Browser
La consecuencia principal es que la interactividad ya no obliga a distribuir toda la aplicación. Hacer interactivo un widget solo implica incluir su código, no el de toda la página.
Desarrollar con Bun en lugar de armar una cadena de herramientas
Elegir Bun no significa afirmar que Node.js está obsoleto. Node cuenta con un ecosistema enorme y es utilizado en una gran parte del JavaScript de producción; sigue siendo un entorno de ejecución excelente. Lo interesante es lo que cambia cuando se diseña un framework alrededor de un entorno de ejecución que ya incluye las herramientas que la mayoría de los proyectos necesitan.
Bun proporciona un entorno de ejecución de JavaScript junto con un gestor de paquetes, un agrupador y un ejecutor de pruebas. Para los creadores de frameworks, esto elimina gran parte de la complejidad adicional. En lugar de colocar un framework encima de Node, luego un gestor de paquetes, después un agrupador separado y finalmente un ejecutor de pruebas distinto, el diseño permite tratar todo ello como una base coherente.
Bun
│
┌────────────┼────────────┐
│ │ │
Runtime Tooling Testing
│ │ │
└────────────┼────────────┘
↓
Stoneware
Es importante aclarar las funciones de cada componente: Bun es la plataforma, mientras que Stoneware es el framework que se implementa sobre ella. Si desea una comparación más amplia de los entornos de ejecución en sí, consulte Node.js, Deno y Bun comparados.
HTML primero, tomado en serio
El renderizado del lado del servidor existe desde hace mucho tiempo, por lo que la idea de “renderizar HTML en el servidor” no parece algo extraordinario. El principio fundamental detrás de Stoneware es que el servidor debe generar HTML verdaderamente útil antes de que el navegador tenga que entender algo sobre la aplicación.
Tomemos un componente que muestra un producto con un título y un precio.
<ProductCard
title="MacBook Pro"
price={1999}
/>
Nada en este componente requiere que el navegador almacene un modelo en JavaScript de él. El servidor puede convertirlo en marcado simple:
<div class="product-card">
<h2>MacBook Pro</h2>
<span>$1999</span>
</div>
Esa salida ya está completa. Las personas pueden leerla, los rastreadores de motores de búsqueda pueden indexarla y el navegador puede mostrarla de inmediato. Ningún script interviene en la entrega del contenido en sí.
Hacer de JavaScript una decisión explícita
Ahora agreguemos un botón de carrito. A diferencia de la ficha del producto, este debe reaccionar a los clics, actualizar el estado y probablemente comunicarse con una API.
<AddToCart product={product} />
Ese componente es donde se justifica el comportamiento del lado del cliente, por lo que se convierte en una isla. El resto de la página permanece como HTML, y la página resultante se ve así:
Product page
│
├── Product title HTML
├── Product description HTML
├── Product image HTML
├── Product specifications HTML
│
└── Add to cart JavaScript island
La página no está “libre de JavaScript”. Está scripteada de forma selectiva, y la selección se realiza por componente en lugar de por página. Esa distinción es importante porque mantiene los costos por defecto bajos y hace que cada fragmento de código distribuido sea una elección visible y deliberada.
Dónde se encuentra el límite de la isla
Una isla marca la línea entre el contenido renderizado en el servidor y el comportamiento que se ejecuta en el cliente. Un sitio de documentación muestra este patrón claramente. El cuerpo del artículo, los títulos, las muestras de código, las imágenes, los enlaces, la navegación y el pie de página son todo contenido. Unas pocas funciones son realmente interactivas: la búsqueda, un cambiador de temas, botones para copiar al portapapeles en los bloques de código y quizás un árbol de navegación expandible.
Documentation page
│
├── Article ─────────────── SSR
├── Code blocks ─────────── SSR
├── Images ──────────────── SSR
├── Navigation ──────────── SSR
│
├── Search ──────────────── Island
├── Theme switcher ──────── Island
└── Copy button ─────────── Island
Este tipo de página, compuesta principalmente por contenido con unas pocas áreas interactivas, es exactamente para lo que está diseñado el framework.
Hidratación versus reanudabilidad
Una objeción razonable es que todo esto no es más que SSR. En parte es cierto: el servidor renderiza HTML. La diferencia radica en lo que ocurre una vez que ese HTML llega.
Con la hidratación clásica, la secuencia es más o menos:
- el servidor envía HTML;
- el navegador descarga el JavaScript de la aplicación;
El navegador termina reconstruyendo una aplicación cuya salida ya ha recibido. Ese trabajo representa un mero gasto adicional desde el punto de vista del usuario: los píxeles ya estaban en la pantalla.
Stoneware, por su parte, está diseñado con islas reanudables. El servidor envía HTML junto con el estado que necesitan esas islas interactivas, y el navegador continúa desde donde dejó el servidor en lugar de volver a ejecutar la página para recuperar ese estado. El objetivo es evitar que pequeñas regiones interactivas obliguen a una reconstrucción completa de la página.
Un objetivo de optimización diferente
La posibilidad de reanudar la ejecución redefine la cuestión del rendimiento. En lugar de preguntarse cómo acelerar la hidratación de todo, se pregunta cuánto trabajo del navegador se puede evitar por completo. La carga útil pasa de “HTML más todo el JavaScript de la aplicación más un paso de hidratación” a “HTML más solo el código necesario para la interacción, reanudando desde el estado generado en el servidor”. Para sitios con mucho contenido, esto suele ser una ventaja mucho mayor que cualquier optimización de hidratación.
Si trabajas con React, la misma presión está detrás de los componentes del servidor; la arquitectura detrás de la renderización sin paquete aborda ese enfoque y realiza una comparación útil.
El enfoque servidor primero no significa solo servidor
Nada de esto constituye un argumento en contra de las aplicaciones del lado del cliente. Los editores colaborativos, las herramientas de diseño, los juegos para navegador y las aplicaciones interactivas complejas tienen necesidades muy diferentes, y la mayoría de sus componentes son interactivos por naturaleza. La arquitectura se dirige al otro extremo del espectro: páginas donde la mayor parte del contenido se renderiza naturalmente en el servidor y la interacción es la excepción.
La exportación estática surge de la misma idea
Si el HTML es el formato por defecto, una pregunta lógica siguiente es por qué debería ejecutarse un servidor para páginas que nunca cambian con cada solicitud. Stoneware puede exportar una aplicación como archivos estáticos y entregarlos a un CDN.
Stoneware application
│
▼
export
│
▼
dist/
│
▼
CDN
La documentación, las páginas de marketing y los catálogos de productos a menudo pueden crearse con antelación y servirse directamente desde el edge. Las rutas que realmente necesitan lógica por solicitud pueden seguir siendo renderizadas en el servidor. La ventaja es que cambiar una ruta de modo estático a SSR no requiere pasar a un modelo de programación diferente para toda la aplicación.
Las rutas dinámicas necesitan una lista explícita de rutas
La exportación estática se vuelve más complicada en cuanto una ruta tiene un parámetro. Una ruta como /products/[sku] podría, en principio, coincidir con un número ilimitado de URLs, por lo que el exportador no puede adivinar qué páginas generar. Por eso, el framework pide a la ruta que las enumere:
export function staticPaths() {
return products.map(product => ({
sku: product.sku
}));
}
Con esa lista, el exportador genera un archivo HTML real para cada SKU conocido, por ejemplo dist/products/laptop-1/index.html. Aquí es donde el diseño del framework va más allá de la renderización de JSX: el framework debe comprender cómo se relacionan las rutas, los datos, el paso de compilación y el destino de despliegue. Un caso límite práctico que hay que planificar es lo que ocurre cuando una ruta dinámica no tiene ningún staticPaths(); el framework debe o bien indicar claramente el error o mantener esa ruta renderizada en el servidor, ya que omitirla silenciosamente es la peor opción.
Los mensajes de error forman parte del renderizador
En esencia, un renderizador toma un componente y genera HTML. La dificultad radica en la enorme variedad de hijos y propiedades que debe manejar: valores de texto y numéricos, arrays y null, elementos y componentes anidados, señales y atributos, errores lanzados, tareas asíncronas y, inevitablemente, valores que no pueden ser renderizados.
Un error común es escribir <span>{product}</span> cuando lo que se pretende es <span>{product.name}</span>. Un mensaje genérico como “no se puede renderizar el valor de tipo objeto” casi no indica dónde buscar. Stoneware, en cambio, describe el valor problemático:
Cannot render a plain object with keys: id, title, price.
y lo sigue con la ruta del componente que llevó a ese valor:
in <span>
in <Price>
in <ProductCard>
in <Home>
Enumerar las claves del objeto sugiere la propiedad que probablemente se buscaba, y el rastro del componente indica el archivo exacto que hay que abrir. Un framework se evalúa no solo por su funcionamiento correcto, sino también por la rapidez con la que ayuda a superar los problemas.
Cuando un microprueba oculta el costo real
Recopilar ese rastro del componente originalmente significaba envolver muchas operaciones de renderizado en try/catch. Una microprueba aislada indicó que el costo adicional era insignificante. Sin embargo, en una renderización de página real, envolver cada elemento hacía que el proceso fuera aproximadamente un 38% más costoso.
La explicación es familiar para quienes realizan pruebas de rendimiento en JavaScript: en una prueba pequeña y repetitiva, el compilador optimizador del motor puede eliminar o reorganizar gran parte del trabajo que se intenta medir, por lo que el resultado refleja una versión optimizada del código.
Los micropruebas pueden engañar cuando el tiempo de ejecución optimiza precisamente aquello que se está midiendo.
La solución fue estructural: la carga adicional por el seguimiento de errores se limitó a los límites de los componentes, y los elementos individuales utilizaron operaciones de guardado y restauración más económicas para mantener el estado. Las pruebas A/B en renders reales no mostraron entonces ninguna diferencia significativa. La lección más amplia es que el rendimiento del renderizador depende tanto de no retroceder al añadir características amigables para los desarrolladores como de la velocidad bruta, y que cualquier afirmación sobre rendimiento debe validarse con cargas de trabajo representativas.
Valores predeterminados de seguridad y orden del pipeline
Una aplicación básica no debería exigir que su desarrollador recuerde cada primitiva de seguridad antes de ponerla en producción. Stoneware viene con valores predeterminados como protección contra CSRF y soporte para políticas de seguridad de contenido.
El orden del pipeline de solicitudes es intencional:
- Primero se ejecuta la protección CSRF;
- a continuación, el middleware de la aplicación;
- después, la coincidencia de rutas y el renderizado;
- todo sale a través de un único punto de salida de respuesta.
Si el middleware de la aplicación se ejecutara antes de la verificación CSRF, el código del usuario podría terminar en una ruta que evita los límites de seguridad a nivel de framework, por ejemplo devolviendo el resultado antes de tiempo o reescribiendo la solicitud. Codificar ese orden en el framework, en lugar de documentarlo y esperar que todos lo sigan, es exactamente el tipo de regla que debería tener un framework. Un único punto de salida también significa que encabezados como CSP se aplican de manera consistente en cada respuesta.
Ampliar un CSP estricto sin desactivarlo
Una política restrictiva es un buen valor por defecto, pero los sitios reales cargan análisis de datos, widgets de pago, APIs, fuentes web y mapas. El modo de fallo más común es que un desarrollador se encuentra con un script bloqueado y desactiva por completo la CSP. Un diseño mejor permite extender directivas individuales manteniendo intacto el resto de la política por defecto:
csp: {
scriptSrc: ["https://www.googletagmanager.com"],
connectSrc: ["https://www.google-analytics.com"],
imgSrc: ["https://www.google-analytics.com"],
}
El principio consiste en valores por defecto seguros, además de una lista de permisos explícita para terceros, directiva por directiva. Al diseñar tal API, decida claramente si una fuente proporcionada se suma a la directiva por defecto o la reemplaza, y documente la respuesta, ya que ambos comportamientos son plausibles y la diferencia tiene consecuencias de seguridad.
Los errores más difíciles ocurren después del renderizador
Un framework no termina en el renderizador. El código debe sobrevivir todo el proceso: desde el código fuente, pasando por el compilador y el renderizador, hasta la construcción del proyecto, los recursos, el despliegue, el CDN y, finalmente, el navegador. Algunos de los problemas más difíciles de resolver aparecen hacia el final de esta cadena, lejos del código que la mayoría de las personas considera como “el framework”.
Empaquetado de recursos para Vercel
La versión 0.1.8 cambió la forma en que Stoneware se despliega en Vercel. Para ese destino, los fragmentos del cliente generados ahora se incluyen en el paquete del servidor como datos en formato base64, lo que los convierte en parte del paquete que la plataforma despliega; además, se sirven desde /_stoneware/*.
Stoneware build
↓
server bundle
├── server code
├── CSS assets
└── island assets
↓
Vercel
↓
/_stoneware/*
El comportamiento es opcional y está limitado al destino Vercel. Una implementación con contenedores ya cuenta con los archivos en el disco y no hay razón para llevar una segunda copia de cada fragmento dentro del paquete del servidor. La conclusión general es que las plataformas de hosting difieren en lo que incluyen en una implementación, por lo que el manejo de recursos suele requerir estrategias específicas para cada destino en lugar de una solución universal.
Pruebas que demuestran la corrección
El proyecto cuenta con más de 500 pruebas automatizadas que abarcan el renderizador, el enrutador, las islas y señales, las hojas de estilo y el servicio de recursos, la exportación estática, CSRF y CSP, los destinos de implementación, el informe de errores, así como entradas hostiles o inusuales como intentos de travesía de rutas, archivos binarios y rutas de recursos mal formadas.
El número en sí no es lo importante. El criterio más útil es este:
Una prueba de regresión debe demostrar que habría fallado antes de la corrección.
Una prueba que pase tanto antes como después de un cambio documenta el comportamiento, pero no protege contra la reaparición del error. Verificar que una nueva prueba falle con el código antiguo es un hábito sencillo que hace que un conjunto de pruebas sea mucho más fiable.
Uso de un agente de programación sin externalizar el diseño
Gran parte de Stoneware se implementó con Claude funcionando como agente de programación. Fue especialmente eficaz para explorar una base de código en expansión, escribir infraestructura repetitiva, generar pruebas, analizar fallos, refactorizar, documentar la arquitectura y probar casos límite.
Lo que no podía hacer por sí solo era decidir qué framework debería utilizarse. Las preguntas difíciles eran de tipo arquitectónico:
- Para cada ruta, ¿es SSR o exportación estática el modo adecuado?
staticPaths()?Un agente puede investigar estas preguntas e implementar la respuesta elegida, pero aún así alguien tiene que cuestionar el diseño y verificar el resultado.
Lo plausible no es lo mismo que lo correcto
Un agente de programación puede crear una implementación convincente muy rápidamente, y esa velocidad es valiosa. Pero también significa que puede cometer errores con la misma rapidez. El problema de los activos en Vercel ilustra este ciclo: la primera solución copió los activos generados en public/, lo cual parecía razonable y superó las pruebas locales; sin embargo, un despliegue real demostró que esa suposición era incorrecta. El segundo intento modificó el propio modelo de despliegue. Las pruebas de casos extremos revelaron luego otro error en esa versión.
Ese ciclo es propio de la ingeniería de software tradicional, no de un fallo de IA. Lo que cambia es la velocidad de cada iteración, lo cual hace que la verificación de extremo a extremo en el entorno real sea aún más importante, y no menos.
Por qué la elección del entorno de ejecución es relevante más allá de la velocidad
Reducir la historia a “Bun es más rápido que Node” pasaría por alto el punto clave, y la velocidad bruta de todos modos no es una filosofía de los frameworks. El experimento más interesante es diseñar un framework partiendo del supuesto de que desde el primer día ya se dispone de un entorno de ejecución moderno y una cadena de herramientas integrada. La combinación en Bun de entorno de ejecución, gestión de paquetes, empaquetado y pruebas la convierte en una base conveniente para ese tipo de exploración arquitectónica.
Dónde encaja la arquitectura
Este enfoque brilla cuando la mayor parte de una página es contenido y solo una parte es interactiva:
- Documentación: los artículos y ejemplos de código se generan en el servidor; la búsqueda, el cambiador de temas y los botones de copiar son elementos independientes.
- Ecomercio: los detalles de los productos, las imágenes y el contenido SEO se generan en el servidor; el carrito de compras y los filtros son elementos independientes.
Dónde probablemente sea la herramienta incorrecta
Si su producto es en realidad una aplicación de escritorio que se ejecuta en un navegador, como un editor colaborativo, una herramienta gráfica, un juego, un panel de control altamente interactivo o cualquier aplicación en la que casi todos los componentes se ejecutan en el cliente, la parte de la página que puede permanecer como HTML es pequeña. En ese caso, el modelo de islas añade limitaciones sin aportar muchos beneficios, y una arquitectura centrada en el cliente suele ser más adecuada. Un framework puede tener una opinión firme sin fingir que se adapta a todo tipo de cargas de trabajo.
Probándolo
En el momento de escribir este texto, la cerámica esculpida sigue en su serie 0.1.x, por lo que se esperan aspectos imperfectos; consulte el repositorio para conocer su estado actual. Las mejoras planificadas incluyen más integraciones, mejores diagnósticos y advertencias de desarrollo, más ejemplos y destinos de despliegue, documentación más completa y más aplicaciones en el mundo real. Para estructurar un proyecto e iniciar el servidor de desarrollo:
bun create stoneware my-app
cd my-app
bun dev
Un buen primer experimento es algo pequeño pero con mucho contenido: un blog, un sitio de documentación, un catálogo de productos, un portafolio o un sitio empresarial. A medida que lo construya, siga preguntándose cuánto de la página realmente necesita JavaScript.
Puntos clave
- Trate el HTML como la salida por defecto y haga que el JavaScript del cliente sea una opción activable por componente; de esta manera, la página tendrá scripts seleccionados en lugar de ser una aplicación completa.
Lecturas relacionadas
- Diseño de API en Node.js con capas: de controladores complejos a arquitectura limpia — Aprenda cómo refactorizar una API de Node.js en capas de controlador, servicio y acceso a datos para solucionar la lógica empresarial enredada, los errores inconsistentes y las dificultades de escalado.
- Funciones incorporadas de Bun 1.4 que pueden reemplazar a sharp, Puppeteer, node-pty y más — Un recorrido práctico por las funciones incorporadas de Bun 1.4 para imágenes, navegadores, Markdown, cron, terminal, scripts paralelos y pruebas, además de cómo probarlas de forma segura en proyectos reales.