Saltar al contenido principal
Volver al Blog
Frontend Arquitectura

Svelte 5 y Runes: Redefiniendo la Reactividad Nativa sin Virtual DOM

Jesús Perera (Pererita)
Jesús Perera
13 de agosto de 2026

Durante la última década, el desarrollo web de alto rendimiento ha estado dominado por un paradigma central: el Virtual DOM (VDOM). Popularizado por React, el VDOM se convirtió en el estándar de facto para reconciliar el estado de la aplicación con la interfaz de usuario. Sin embargo, este enfoque conlleva un costo de computación no despreciable en el cliente (tiempo de ejecución dedicado al diffing de árboles jerárquicos y recolección de basura).

Svelte nació con una tesis disruptiva: desplazar el trabajo pesado del cliente al compilador. No obstante, las versiones 3 y 4 de Svelte, aunque revolucionarias, dependían de un análisis estático limitado al ámbito de un solo componente físico (archivos .svelte). Con la llegada de Svelte 5, el framework introduce Runes (Runas), un nuevo modelo de reactividad universal basado en señales (signals) que unifica el estado dentro y fuera de los componentes, prescindiendo por completo del Virtual DOM y optimizando la reactividad a un nivel atómico.


1. El Dilema del VDOM y la Reactividad por Compilación

Para entender la relevancia de Svelte 5, es imperativo analizar las limitaciones arquitectónicas de las soluciones tradicionales:

  • La ineficiencia del Virtual DOM: Frameworks como React o Vue (en su núcleo tradicional) no saben qué parte exacta de la interfaz ha cambiado. En su lugar, generan un árbol virtual completo, lo comparan con la versión anterior y determinan los cambios. A gran escala, este proceso consume valiosos ciclos de CPU y memoria.
  • Las limitaciones de la reactividad por asignación (Svelte 3 y 4): Svelte eliminó el VDOM compilando el código directamente a operaciones del DOM. No obstante, su reactividad dependía de la asignación simple (count += 1) y la sintaxis no estándar de JavaScript ($: doubled = count * 2). Esto generaba un límite estricto: la reactividad no podía salir del archivo de componente de manera natural, obligando a los arquitectos de software a recurrir a soluciones complejas como los stores (writable, readable, derived) para manejar el estado global.

El paradigma de las Señales en Svelte 5

Svelte 5 introduce las Runes, un conjunto de primitivas que actúan como instrucciones directas para el compilador. Detrás de escena, las Runas implementan un motor de señales finas (fine-grained signals). En lugar de recalcular componentes o buscar diferencias en estructuras virtuales, las Runas crean un grafo de dependencias en tiempo de ejecución altamente optimizado. Cuando un dato muta, solo las funciones u hojas del DOM suscritas a esa señal específica se ejecutan de forma directa e inmediata.


2. Conceptos Clave & Arquitectura de Svelte 5

La arquitectura de reactividad nativa de Svelte 5 se sustenta en tres pilares conceptuales representados por sus runas principales.

Las Tres Runas Fundamentales

  1. $state(value): Declara un estado reactivo. A diferencia de las variables let tradicionales en Svelte 4, $state utiliza internamente proxies JavaScript que permiten capturar operaciones de lectura y escritura de forma profunda en objetos y arrays.
  2. $derived(expression): Representa un estado derivado o computado. Svelte analiza automáticamente las dependencias dentro de la expresión. Si alguna de las señales internas cambia, el valor derivado se recalcula de forma perezosa (lazy evaluation).
  3. $effect(fn): Gestiona efectos secundarios. Reemplaza los ciclos de vida tradicionales (onMount, onDestroy, afterUpdate) y la sintaxis reactiva anterior ($: console.log(...)). Los efectos se ejecutan en el cliente una vez que el DOM se ha actualizado de manera asíncrona.

Flujo y Propagación en el Grafo Reactivo

El flujo de actualización en Svelte 5 sigue un ciclo puramente unidireccional y predecible, gestionado mediante una cola de microtareas:

  1. Declaración e Inicialización: El compilador intercepta el uso de $state() y lo transforma en una señal reactiva que encapsula el valor interno y registra a los lectores activos en un grafo de dependencias.
  2. Registro de Dependencias (Suscripción Automática): Cuando una función de renderizado del DOM o una runa $derived ejecuta el lector de una señal, esta se registra dinámicamente como dependiente en el contexto activo de ejecución.
  3. Mutación: Al modificar el valor del estado, el setter del proxy intercepta la operación y notifica al planificador (scheduler) del framework que el nodo raíz del grafo ha cambiado.
  4. Batching y Planificación de Microtareas: El motor de Svelte 5 encola las actualizaciones pendientes. En lugar de mutar el DOM de forma síncrona por cada cambio menor, espera al final del ciclo de microtareas actual para aplicar todos los cambios acumulados.
  5. Actualización Fina del DOM: El runtime ejecuta únicamente las funciones de actualización directa del DOM asociadas a las dependencias marcadas como sucias, sin afectar a ningún nodo adyacente del árbol.

3. Ejemplo Práctico: Estado Compartido Universal

Uno de los mayores beneficios arquitectónicos de Svelte 5 es la capacidad de extraer lógica reactiva compleja a archivos TypeScript puros (.svelte.ts), permitiendo una inyección de dependencias limpia y escalable.

A continuación, implementaremos un sistema de gestión de un carrito de compras que comparte estado global de forma nativa sin utilizar stores.

Paso 1: Definición del Servicio de Estado (cart.svelte.ts)

// cart.svelte.ts

export interface Product {
  id: string;
  name: string;
  price: number;
}

export interface CartItem {
  product: Product;
  quantity: number;
}

class CartService {
  // Definimos el estado interno como reactivo profundo usando la runa $state
  #items = $state<CartItem[]>([]);

  // Runa $derived para calcular valores derivados en tiempo de ejecución
  totalItems = $derived(() =>
    this.#items.reduce((acc, item) => acc + item.quantity, 0)
  );

  totalPrice = $derived(() =>
    this.#items.reduce(
      (acc, item) => acc + item.product.price * item.quantity,
      0
    )
  );

  // Exponemos el estado mediante un getter de solo lectura para proteger la inmutabilidad externa
  get items() {
    return this.#items;
  }

  // Métodos para mutar el estado de manera segura
  addProduct(product: Product) {
    const existingItem = this.#items.find(
      (item) => item.product.id === product.id
    );

    if (existingItem) {
      existingItem.quantity += 1;
    } else {
      // Svelte 5 detecta la mutación del array de forma nativa
      this.#items.push({ product, quantity: 1 });
    }
  }

  removeProduct(productId: string) {
    this.#items = this.#items.filter((item) => item.product.id !== productId);
  }
}

// Exportamos una instancia única para simular un Singleton global
export const cartService = new CartService();

Paso 2: Componente Consumidor (CartView.svelte)

<!-- CartView.svelte -->
<script lang="ts">
  import { cartService } from './cart.svelte';

  // Definición de propiedades locales usando destructuración con $props() en Svelte 5
  let { title = 'Detalle del Carrito' } = $props<{ title?: string }>();

  // Efecto secundario para sincronizar el estado local con persistencia externa (LocalStorage)
  $effect(() => {
    localStorage.setItem(
      'shopping_cart_data',
      JSON.stringify(cartService.items)
    );
    console.log(`Carrito actualizado: ${cartService.totalItems()} productos.`);

    // Función de limpieza opcional (cleanup)
    return () => {
      console.log('Limpiando subscripción del efecto');
    };
  });
</script>

<div class="cart-container">
  <h2>{title}</h2>

  {#if cartService.items.length === 0}
  <p>El carrito está vacío.</p>
  {:else}
  <ul>
    {#each cartService.items as item (item.product.id)}
    <li>
      <span>{item.product.name} (x{item.quantity})</span>
      <span>${(item.product.price * item.quantity).toFixed(2)}</span>
      <button onclick="{()" ="">
        cartService.removeProduct(item.product.id)}> Eliminar
      </button>
    </li>
    {/each}
  </ul>

  <div class="summary">
    <p>Total Productos: <strong>{cartService.totalItems()}</strong></p>
    <p>
      Importe Total: <strong>${cartService.totalPrice().toFixed(2)}</strong>
    </p>
  </div>
  {/if}
</div>

<style>
  .cart-container {
    padding: 1.5rem;
    border: 1px solid #eaeaea;
    border-radius: 8px;
  }
  .summary {
    margin-top: 1rem;
    border-top: 2px solid #333;
    padding-top: 0.5rem;
  }
</style>

4. Ventajas, Desafíos y Trade-offs Técnicos

El cambio de paradigma hacia las Runes es un movimiento audaz de los mantenedores de Svelte. Como arquitecto de software, es fundamental evaluar las ventajas competitivas frente a los costos de implementación.

Ventajas Técnicas

  • Rendimiento en tiempo de ejecución: Al eliminar la reconciliación del Virtual DOM, el renderizado de listas masivas (keyed each blocks) y actualizaciones de UI complejas se ejecutan en microsegundos, reduciendo el consumo de batería en dispositivos móviles.
  • Reactividad verdaderamente universal: No hay distinción entre la reactividad dentro de un archivo .svelte y un archivo TypeScript .ts. Esto simplifica la arquitectura limpia (Clean Architecture) al desacoplar las reglas de negocio de la UI de presentación.
  • Adiós a la complejidad de las APIs de Stores: Desaparece la necesidad de usar sintaxis engorrosas como $store para la desreferenciación. El estado se consume utilizando la sintaxis de JavaScript nativo (getters/setters).
  • Tamaño del Bundle Reducido: El compilador genera menos código de apoyo (boilerplate) que en versiones anteriores de Svelte gracias a que el sistema de señales unifica la lógica de actualización.

Desafíos y Desventajas

  • Curva de aprendizaje para desarrolladores existentes: Los desarrolladores acostumbrados a la reactividad implícita de Svelte 3/4 (let y $:) deben adaptar su mentalidad hacia el uso explícito de funciones/Runas.
  • Migración de código legacy: Adaptar proyectos empresariales robustos de Svelte 4 a Svelte 5 requiere planificación táctica, ya que el modelo mental de los ciclos de vida (onMount, onDestroy) cambia drásticamente en favor de $effect.
  • Dependencia en JavaScript Moderno: El motor de reactividad profunda de $state requiere soporte nativo de Proxies en el navegador, lo que descarta de forma definitiva la compatibilidad con navegadores obsoletos.

5. Conclusión y Perspectivas Futuras

La introducción de Runes en Svelte 5 consolida una tendencia de la industria del desarrollo de software: la convergencia hacia las Señales (Signals). Frameworks de gran escala como Angular (con sus nuevos signals) y SolidJS ya han demostrado que la granularidad fina es el camino óptimo para lograr interfaces web de ultra-alto rendimiento.

Svelte 5 lleva esto un paso más allá gracias a su enfoque híbrido. Al mantener un compilador inteligente, puede optimizar la declaración de señales en tiempo de construcción, eliminando la necesidad de crear envolturas manuales de bajo nivel en cada declaración.

Para arquitectos e ingenieros que diseñan plataformas web de alto tráfico, Svelte 5 representa la maduración definitiva de la reactividad compilada. Es una herramienta potente que permite mantener el código altamente legible, modularizado bajo patrones clásicos de diseño de software (como inyección de dependencias o arquitecturas orientadas a servicios), todo mientras ofrece un rendimiento nativo casi idéntico al de JavaScript puro sobre el DOM de producción.