Next-Gen Reactivity Rethink Preact SolidJS Signals vs Svelte 5 Runes
Min-jun Kim
Dev Intern · Leapcell

Introduction
The landscape of frontend development is in a constant state of evolution, with developers continually seeking more efficient and intuitive ways to manage application state. For years, frameworks have grappled with the challenge of building user interfaces that respond seamlessly to data changes. This pursuit has led to a fascinating convergence on a powerful new paradigm: fine-grained reactivity. This shift promises to dramatically reduce re-renders, simplify state management, and ultimately enhance application performance and developer experience. Today, we're witnessing a pivotal moment with the emergence of refined reactive primitives in frameworks like Preact and SolidJS, championed by their "Signals" implementation, and Svelte's ambitious "Runes" in its upcoming version 5. These innovations are not just incremental updates; they represent a fundamental rethinking of how we build dynamic web applications. Understanding their underlying mechanisms and practical implications is crucial for any frontend developer aiming to stay at the forefront of the industry. This article will dissect these two approaches, exploring their core concepts, practical applications, and what they mean for the future of reactive programming.
The Core of Modern Reactivity
Before we dive into the specifics of Signals and Runes, it's essential to understand the foundational concepts that underpin them.
Reactive Primitive
A reactive primitive is the smallest, atomic unit of reactive state. It's a value that, when changed, automatically notifies any code that depends on it. This forms the basis of highly efficient updates, as only the parts of the UI directly affected by the change are re-rendered or re-computed.
Fine-Grained Reactivity
Traditionally, frameworks might re-render entire components when a state change occurs, even if only a small part of that component's UI is affected. Fine-grained reactivity, however, allows for updates at a much more granular level. When a reactive primitive changes, only the specific expressions or DOM nodes that directly depend on that primitive are re-evaluated or updated. This contrasts sharply with coarser-grained systems that might re-evaluate larger component trees.
Auto-Subscriptions
A key enabler of fine-grained reactivity is the concept of auto-subscriptions. When you read a reactive primitive within a reactive context (e.g., a component render function, an effect, or a computed property), the system automatically "subscribes" to that primitive. This means it implicitly tracks dependencies without explicit subscription management code from the developer.
Memoization/Computed Values
To further optimize performance, reactive systems often provide mechanisms for memoizing or computing derived values. These are functions whose output is cached and only re-executed when their underlying reactive dependencies change. This prevents redundant computations and ensures efficiency.
Signals: The Preact and SolidJS Approach
Signals, popularized by SolidJS and adopted by Preact and others, represent a straightforward and highly performant approach to fine-grained reactivity.
Principles
The core idea behind Signals is simple: a "signal" is an object with a value
property that holds a piece of state. When you read its value
, you create a dependency. When you write to its value
, it notifies all its dependents. Signals are distinct from components; they are independent units of state.
Implementation
Let's look at a simple example using Preact Signals.
// Preact Signals import { signal, computed, effect } from '@preact/signals-react'; // Create a signal const count = signal(0); const name = signal('World'); // Create a computed signal (derived state) const greeting = computed(() => `Hello, ${name.value}! The count is ${count.value}.`); // Create an effect (side effect) effect(() => { console.log('Current greeting:', greeting.value); }); // Update signals count.value++; // Console logs: "Current greeting: Hello, World! The count is 1." name.value = 'Preact'; // Console logs: "Current greeting: Hello, Preact! The count is 1." count.value++; // Console logs: "Current greeting: Hello, Preact! The count is 2."
In this example:
signal(value)
creates a new reactive primitive. You access its value via.value
.computed(() => ...)
creates a new signal whose value is derived from other signals. It automatically re-evaluates only when its dependencies (name.value
,count.value
) change.effect(() => ...)
registers a side effect that automatically re-runs whenever its dependencies (greeting.value
) change.
When integrating with a rendering framework like Preact or React, components can use these signals directly.
// Preact component using signals import { signal } from '@preact/signals-react'; const counter = signal(0); function Counter() { return ( <div> <p>Count: {counter.value}</p> <button onClick={() => counter.value++}>Increment</button> </div> ); } // In a Preact application, this component would re-render only the affected text node, // not the entire component function, when counter.value changes.
This fine-grained approach means that when counter.value
updates, Preact's renderer can efficiently pinpoint and update just the text node displaying the count, without re-executing the Counter
function unnecessarily.
Application Scenarios
Signals excel in scenarios requiring extremely high performance and fine-grained updates, such as:
- Interactive dashboards and data visualizations: Rapid updates to specific data points without re-rendering large charts.
- Complex forms with interdependent fields: Immediate feedback on field validation or derived calculations.
- Real-time applications: Chat apps, collaborative tools where immediate UI reactions are critical.
- State management at scale: Replacing more complex state management solutions with simpler, performant signals.
Runes: The Svelte 5 Revelation
Svelte has always been known for its reactive compiler. With Svelte 5 and "Runes," this reactivity is being fundamentally re-architected and, in many ways, brought closer to the explicit, signal-like mechanisms seen in SolidJS while retaining Svelte's compiler-driven magic.
Principles
Svelte 5 Runes introduce explicit reactive primitives directly into the Svelte language. Unlike previous Svelte versions where reactivity was implicit (via assignments), Runes make reactivity explicit through special syntax or functions. This new approach aims for improved performance, better debuggability, and more direct control over reactivity flow.
Implementation
The core Runes are $state
, $derived
, and $effect
.
<!-- Svelte 5 Runes Example --> <script> import { $state, $derived, $effect } from 'svelte'; // Not strictly needed in <script> tags for basic use // Create a reactive state let count = $state(0); let name = $state('World'); // Create a derived state let greeting = $derived(() => `Hello, ${name}! The count is ${count}.`); // Create an effect $effect(() => { console.log('Current greeting:', greeting); // Effects can also have cleanup functions return () => console.log('Cleanup for:', greeting); }); function increment() { count++; } function changeName() { name = 'Svelte'; } </script> <h1>{greeting}</h1> <button on:click={increment}>Increment Count</button> <button on:click={changeName}>Change Name</button>
Here's how Runes map to the core concepts:
$state(value)
defines a new reactive piece of state. Like signals, it immediately notifies dependents upon change. Unlike signals'.value
syntax, Svelte's compiler allows direct variable access (count
) within the component's template and script.$derived(() => ...)
defines a derived value. It's similar tocomputed
in signals – it re-executes its callback only when its dependencies (name
,count
) change.$effect(() => ...)
defines a side effect that automatically re-runs when its dependencies (greeting
) change.
The compiler's role is still paramount. It transforms this concise syntax into highly optimized JavaScript, ensuring efficient updates.
Application Scenarios
Svelte 5 Runes target a broad range of applications, much like Signals, but with Svelte's signature developer experience:
- Any Svelte application: Runes are poised to become the default and recommended way to manage state in Svelte 5, leading to more consistent and performant Svelte apps.
- Enhanced component logic: More complex internal component state management becomes cleaner and more efficient.
- Server-side rendering (SSR) and client-side hydration: Runes bring inherent advantages for efficient hydration due to their explicit reactivity graph.
- Libraries and reusable components: Authors can build highly optimized and resilient components with predictable reactivity.
Comparing the Approaches
While both Signals and Runes aim to achieve fine-grained reactivity and share similar primitives, their execution and developer experience diverge.
Explicit vs. Compiler-Driven
- Signals: By their nature, Signals are explicit. You interact with
signal.value
everywhere. This explicitness can make the reactivity graph easier to reason about in isolation, as the signal object itself carries the reactive state. - Runes: Svelte's Runes are a blend. While
$state
,$derived
, and$effect
are explicit declarations, the tracking of dependencies and the optimizations are still heavily compiler-driven. You declarelet count = $state(0);
and then just usecount
directly, relying on the compiler to handle the reactive plumbing. This offers a more "vanilla JavaScript" feel while retaining powerful reactivity.
Scope of Reactivity
- Signals: Signals are framework-agnostic at their core. They can be used in React, Preact, Vue, or even vanilla JavaScript. Their integration into frameworks like Preact makes them first-class citizens, but their fundamental nature is external.
- Runes: Runes are deeply integrated into the Svelte compiler and language. They are designed to work within the Svelte ecosystem and leverage its unique compilation model. This integration allows for certain optimizations and a streamlined developer experience unique to Svelte.
Performance Characteristics
Both approaches aim for optimal performance through fine-grained updates, largely avoiding Virtual DOM diffing costs for reactive parts.
- Signals: SolidJS, the primary proponent of Signals, is renowned for its raw performance, often topping benchmarks. This is due to its highly efficient updates that bypass Virtual DOM entirely for reactive parts. Preact's implementation brings similar benefits to the React ecosystem, offering an opt-in performance boost.
- Runes: Svelte has always been a top performer due to its compile-time optimizations. Runes are expected to further enhance this by providing a more powerful and explicit reactivity graph for the compiler to optimize, potentially leading to even better benchmarks by making the reactivity tracking more precise and less heuristic-based than previous Svelte versions.
Developer Experience
- Signals: The
.value
syntax can be an initial adjustment for developers coming from React'suseState
or Svelte 4's implicit reactivity. However, once accustomed, it offers clarity regarding when a value is reactive. - Runes: Svelte 5 aims for a less verbose syntax while maintaining explicit reactivity. The ability to use
let count = $state(0);
and then justcount
directly is very appealing, reducing syntactic noise. This can feel more natural to developers familiar with standard JavaScript variables.
Conclusion
Both Preact/SolidJS Signals and Svelte 5 Runes represent significant leaps forward in frontend reactivity, each offering compelling advantages. Signals provide a highly explicit, framework-agnostic primitive that emphasizes direct control and raw performance, making them excellent for integrating fine-grained reactivity into existing component-based ecosystems or building highly optimized applications from the ground up. Svelte 5 Runes, on the other hand, embrace similar fine-grained primitives but deeply embed them within Svelte's powerful compiler, offering a seamless, "magic-like" developer experience with robust performance.
Ultimately, the choice between these approaches often boils down to framework preference and the specific needs of a project. Yet, they both underscore a clear trend: frontend frameworks are evolving towards more efficient, explicit, and performant state management, leading to web applications that are faster, smoother, and more delightful for users. The future of reactive programming is fine-grained, explicit, and highly performant.