Svelte 5 y Runes: La Revolución de la Reactividad en JavaScript en 2025
Hola HaWkers, Svelte 5 llegó con un cambio paradigmático que está haciendo a la comunidad JavaScript repensar cómo abordamos reactividad en aplicaciones web. Los Runes representan una nueva forma de pensar sobre estado y reactividad, y si aún no los conoces, estás perdiendo una de las innovaciones más interesantes del ecosistema frontend.
¿Ya te preguntaste por qué necesitamos tantos hooks y boilerplate para gerenciar estado en React? Svelte 5 ofrece una alternativa elegante.
Qué Son los Runes
Runes son una nueva primitiva de reactividad introducida en Svelte 5. Ellos sustituyen el sistema de reactividad anterior basado en declaraciones reactivas ($:) por un modelo más explícito y poderoso.
Los Principales Runes
Lista de Runes disponibles:
$state- Declara estado reactivo$derived- Computa valores derivados$effect- Ejecuta side effects reactivos$props- Recibe props en componentes$bindable- Props que pueden ser two-way bound$inspect- Debug de valores reactivos
Comparación: Svelte 4 vs Svelte 5
Vamos a ver en la práctica cómo la sintaxis cambió y por qué es una mejora significativa.
Estado Simple
Svelte 4 (antiguo):
<script>
let count = 0;
function increment() {
count += 1;
}
</script>
<button on:click={increment}>
Clics: {count}
</button>Svelte 5 (Runes):
<script>
let count = $state(0);
function increment() {
count += 1;
}
</script>
<button onclick={increment}>
Clics: {count}
</button>La diferencia parece sutil, pero el $state torna la reactividad explícita y previsible.
Valores Derivados
Svelte 4 (antiguo):
<script>
let firstName = 'John';
let lastName = 'Doe';
// Declaración reactiva - puede ser confusa
$: fullName = `${firstName} ${lastName}`;
$: greeting = `Hola, ${fullName}!`;
</script>Svelte 5 (Runes):
<script>
let firstName = $state('John');
let lastName = $state('Doe');
// Mucho más claro y previsible
let fullName = $derived(`${firstName} ${lastName}`);
let greeting = $derived(`Hola, ${fullName}!`);
</script>
Effects (Side Effects)
Svelte 4 (antiguo):
<script>
let count = 0;
// Effect implícito - difícil saber cuándo ejecuta
$: {
console.log(`Count cambió para: ${count}`);
localStorage.setItem('count', count);
}
</script>Svelte 5 (Runes):
<script>
let count = $state(0);
// Effect explícito - claro cuándo y por qué ejecuta
$effect(() => {
console.log(`Count cambió para: ${count}`);
localStorage.setItem('count', count.toString());
});
</script>Ejemplo Práctico: Todo App con Runes
Vamos a construir una aplicación de tareas completa usando Runes:
<script>
// Estado principal
let todos = $state([]);
let newTodo = $state('');
let filter = $state('all'); // 'all' | 'active' | 'completed'
// Valores derivados
let filteredTodos = $derived(() => {
switch (filter) {
case 'active':
return todos.filter(t => !t.completed);
case 'completed':
return todos.filter(t => t.completed);
default:
return todos;
}
});
let remainingCount = $derived(
todos.filter(t => !t.completed).length
);
let allCompleted = $derived(
todos.length > 0 && todos.every(t => t.completed)
);
// Funciones de acción
function addTodo() {
if (newTodo.trim()) {
todos.push({
id: Date.now(),
text: newTodo.trim(),
completed: false
});
newTodo = '';
}
}
function toggleTodo(id) {
const todo = todos.find(t => t.id === id);
if (todo) {
todo.completed = !todo.completed;
}
}
function removeTodo(id) {
const index = todos.findIndex(t => t.id === id);
if (index !== -1) {
todos.splice(index, 1);
}
}
function toggleAll() {
const newState = !allCompleted;
todos.forEach(t => t.completed = newState);
}
function clearCompleted() {
todos = todos.filter(t => !t.completed);
}
// Effect para persistencia
$effect(() => {
localStorage.setItem('todos', JSON.stringify(todos));
});
// Cargar del localStorage en la inicialización
$effect(() => {
const saved = localStorage.getItem('todos');
if (saved) {
todos = JSON.parse(saved);
}
});
</script>
<div class="todo-app">
<h1>Mis Tareas</h1>
<form onsubmit={(e) => { e.preventDefault(); addTodo(); }}>
<input
type="text"
bind:value={newTodo}
placeholder="¿Qué necesita ser hecho?"
/>
<button type="submit">Agregar</button>
</form>
{#if todos.length > 0}
<div class="controls">
<button onclick={toggleAll}>
{allCompleted ? 'Desmarcar' : 'Marcar'} Todas
</button>
<div class="filters">
<button
class:active={filter === 'all'}
onclick={() => filter = 'all'}
>
Todas
</button>
<button
class:active={filter === 'active'}
onclick={() => filter = 'active'}
>
Activas
</button>
<button
class:active={filter === 'completed'}
onclick={() => filter = 'completed'}
>
Completas
</button>
</div>
</div>
<ul class="todo-list">
{#each filteredTodos as todo (todo.id)}
<li class:completed={todo.completed}>
<input
type="checkbox"
checked={todo.completed}
onchange={() => toggleTodo(todo.id)}
/>
<span>{todo.text}</span>
<button onclick={() => removeTodo(todo.id)}>X</button>
</li>
{/each}
</ul>
<footer>
<span>{remainingCount} tarea(s) restante(s)</span>
{#if todos.some(t => t.completed)}
<button onclick={clearCompleted}>
Limpiar Completas
</button>
{/if}
</footer>
{/if}
</div>
Por Qué Runes Son Mejores
1. Reactividad Explícita
En Svelte 4, cualquier variable let era automáticamente reactiva, lo que podía causar confusión:
<!-- Svelte 4: ¿Cuándo esto es reactivo? -->
<script>
let data = fetch('/api').then(r => r.json()); // ¿Reactivo?
let config = { theme: 'dark' }; // ¿Reactivo?
const MAX = 100; // No reactivo (const)
</script>Con Runes, declaras explícitamente lo que es reactivo:
<!-- Svelte 5: Claridad total -->
<script>
let data = $state(null); // Reactivo
let config = $state({ theme: 'dark' }); // Reactivo
const MAX = 100; // No reactivo (intencional)
</script>2. Mejor TypeScript Support
Runes funcionan perfectamente con TypeScript:
<script lang="ts">
interface Todo {
id: number;
text: string;
completed: boolean;
}
let todos = $state<Todo[]>([]);
let newTodo = $state('');
// Tipos inferidos correctamente
let completedCount = $derived(
todos.filter(t => t.completed).length
);
</script>3. Reactividad Universal
Runes funcionan fuera de componentes .svelte:
// stores/counter.svelte.ts
export function createCounter(initial = 0) {
let count = $state(initial);
return {
get value() { return count; },
increment() { count += 1; },
decrement() { count -= 1; },
reset() { count = initial; }
};
}
// En cualquier componente
import { createCounter } from './stores/counter.svelte';
const counter = createCounter(10);
Comparación con React
Vamos a comparar la misma funcionalidad en React y Svelte 5:
React (Hooks):
import { useState, useMemo, useEffect } from 'react';
function Counter() {
const [count, setCount] = useState(0);
const [step, setStep] = useState(1);
// useMemo para valores derivados
const doubled = useMemo(() => count * 2, [count]);
// useEffect para side effects
useEffect(() => {
document.title = `Count: ${count}`;
}, [count]);
return (
<div>
<p>Count: {count} (doubled: {doubled})</p>
<input
type="number"
value={step}
onChange={e => setStep(Number(e.target.value))}
/>
<button onClick={() => setCount(c => c + step)}>
Increment by {step}
</button>
</div>
);
}Svelte 5 (Runes):
<script>
let count = $state(0);
let step = $state(1);
// Derivado - sin array de dependencias
let doubled = $derived(count * 2);
// Effect - sin array de dependencias
$effect(() => {
document.title = `Count: ${count}`;
});
</script>
<div>
<p>Count: {count} (doubled: {doubled})</p>
<input type="number" bind:value={step} />
<button onclick={() => count += step}>
Increment by {step}
</button>
</div>Diferencias notables:
- Sin arrays de dependencias (menos bugs)
- Sintaxis más concisa
- Binding bidireccional nativo
- Menos boilerplate
Performance de Svelte 5
Svelte 5 trae mejoras significativas de performance:
Benchmarks (js-framework-benchmark)
| Métrica | React 19 | Vue 3.5 | Svelte 5 |
|---|---|---|---|
| Create 1000 rows | 45ms | 38ms | 32ms |
| Update 1000 rows | 38ms | 32ms | 28ms |
| Select row | 3.2ms | 2.8ms | 2.1ms |
| Remove row | 35ms | 30ms | 25ms |
| Create 10000 rows | 450ms | 380ms | 320ms |
| Bundle size (gzip) | 45KB | 35KB | 3KB |
⚡ Destaque: Svelte compila para JavaScript vanilla, resultando en bundles significativamente menores.
Migrando de Svelte 4 a Svelte 5
Estrategia de Migración
- Actualiza Svelte:
npm install svelte@5 - Código existente funciona: Svelte 5 es retrocompatible
- Migra gradualmente: Convierte componentes uno por vez
- Usa el modo legacy:
<svelte:options runes={false} />
Herramienta de Migración
Svelte ofrece una herramienta de migración automática:
# Migrar archivos automáticamente
npx sv migrate svelte-5
# Verificar problemas de compatibilidad
npx svelte-checkPatrones de Migración
Stores para Runes:
// Svelte 4: Stores
import { writable, derived } from 'svelte/store';
export const count = writable(0);
export const doubled = derived(count, $count => $count * 2);
// Svelte 5: Runes (stores aún funcionan!)
// Pero Runes son recomendados para código nuevo
let count = $state(0);
let doubled = $derived(count * 2);Cuándo Usar Svelte 5
Usa Svelte 5 cuando:
- Inicias nuevo proyecto frontend
- Necesitas performance máxima
- Quieres menos boilerplate
- Prefieres sintaxis más próxima a JavaScript vanilla
- Necesitas bundles menores
Considera otras opciones cuando:
- Equipo muy experimentado en React/Vue
- Ecosistema de bibliotecas es crucial
- Necesitas SSR complejo (SvelteKit aún es más joven)
Conclusión
Svelte 5 con Runes representa una evolución significativa en la forma como pensamos sobre reactividad en JavaScript. La sintaxis explícita, el mejor soporte a TypeScript y la performance superior hacen de él una elección atractiva para nuevos proyectos.
Si ya trabajas con React o Vue, los conceptos de Runes serán familiares, pero la implementación es más elegante y directa. Vale la pena experimentar en un proyecto personal para sentir la diferencia.
La comunidad JavaScript continúa innovando, y frameworks como Svelte muestran que aún hay mucho espacio para mejoras en la forma como construimos aplicaciones web.
Si quieres explorar más sobre frameworks JavaScript modernos, te recomiendo echar un vistazo al artículo sobre React vs Vue en 2025: Cuál Framework Elegir donde comparamos las principales opciones del mercado.

