Web Development · Frontend Architecture
Signals Explained: How Fine-Grained Reactivity Works
Signals are the reactivity model behind Angular, Vue, Solid, and Svelte 5. Here's how they actually work, and the TC39 proposal to standardize them.
Abhishek Gupta
6 min read
Sponsored
A signal is a container for a value that knows who’s reading it. That sentence is the entire idea, and it’s also why Angular, Vue, Solid, Preact, and Svelte have all quietly converged on some version of it while React still hasn’t. Read a signal, and you’re automatically registered as a dependent. Change the signal, and only the things that actually read it update, not a whole component, not a subtree, just the specific piece of work that depends on that specific value.
What is a signal in JavaScript?
A signal is a reactive value container that automatically tracks its own dependents. When code reads a signal, that code is registered as depending on it. When the signal’s value changes, only its registered dependents re-run, which is what lets a framework update the exact part of the UI that changed without re-checking anything else. It’s the mechanism underneath what frameworks call “fine-grained reactivity.”
The two pieces that make it work
Strip away the framework-specific syntax and there are really just two primitives worth understanding. A state signal holds a value you can read and write directly, the reactive equivalent of a variable. A computed signal derives its value from other signals, recalculating automatically whenever any signal it reads changes, without you writing a single subscription or cleanup call. You read a computed signal the same way you read a state signal; the dependency tracking happens for you, silently, every time you read one inside another.
That automatic tracking is the actual innovation, not the “signal” name. Older reactive patterns, observables, pub-sub event buses, made you manually subscribe to a source and manually unsubscribe when you were done, and forgetting the unsubscribe was a reliable way to leak memory for a decade of frontend development. Signals remove that step. The framework watches which signals get read during a computed signal’s or a component’s execution and wires up the dependency graph as a side effect of just running the code normally.
How the frameworks actually differ
Here’s where it gets interesting, because “signals” isn’t one implementation, it’s a shared goal that frameworks reached by genuinely different routes.
| Framework | Mechanism | Where the work happens |
|---|---|---|
| Solid | Runtime signal objects, the original popularizer of this pattern for the web | Runtime, on every read |
| Preact Signals | Runtime signal objects, framework-agnostic, usable outside Preact too | Runtime |
| Vue | ref() and computed() wrapping the same dependency-tracking core | Runtime, abstracted behind familiar APIs |
| Angular (17+) | Signals as a first-class primitive, replacing zone-based change detection | Runtime |
| Svelte 5 | Runes, syntactically similar to signals but compiled away entirely | Build time, via the compiler |
Svelte’s approach is the one that trips people up, and a year of production data on Svelte 5’s runes backs this up: runes look like they’re doing the same runtime tracking Solid does, but by the time Svelte’s compiler finishes, there’s no signal object left in the shipped code at all. The compiler analyzed which values depend on which, and generated direct DOM-update instructions for exactly those relationships. Solid pays a small runtime cost to track dependencies as the app runs; Svelte pays that cost once, at build time, and ships something closer to hand-written imperative DOM code. Different tradeoff, same end result: update exactly what changed, skip everything else.
Vue took a third path worth naming separately: Vue 3.6’s Vapor Mode compiles templates straight to DOM operations too, dropping the virtual DOM entirely rather than just wrapping signals in a familiar API. And on the runtime-signals side, Angular 21 made signals the default alongside zoneless change detection, replacing the framework’s older zone.js-based dirty-checking with the same read-tracks-dependent model Solid pioneered.
React sits outside this convergence on purpose. Its model re-runs a whole component function on a state change and reconciles a virtual DOM against the previous render to figure out what to touch in the real DOM. That’s coarser-grained by design, and React’s answer to the performance question has been compiler-driven automatic memoization rather than adopting fine-grained signals, keeping the mental model (state changes, function re-runs) the same while a build tool quietly skips unnecessary work behind the scenes.
The part most explainers skip: TC39 wants to standardize this
Every framework in that table built its own version of the same underlying idea, which means the same dependency-tracking logic has been reinvented five separate times with five incompatible APIs. TC39, the committee that standardizes JavaScript itself, has a Signals proposal aimed at fixing exactly that, currently sitting at Stage 1, meaning the committee has agreed the problem is worth solving and is exploring solutions, not yet locked into a final API.
What makes this proposal worth watching, more than most Stage 1 proposals, is who’s behind it. Champions Daniel Ehrenberg (Igalia) and Rob Eisenberg (FAST, Aurelia) are coordinating with maintainers and authors from Angular, Vue, Solid, Preact, Ember, Qwik, and MobX, all contributing to one shared spec instead of defending their own implementation. That’s an unusual level of cross-framework agreement for something this early in the process, and it exists because every one of those projects is currently maintaining a proprietary reactivity system that does roughly the same job as everyone else’s.
The proposal itself stays narrow on purpose: just a State signal and a Computed signal, automatic dependency tracking, and explicitly nothing about rendering, effect scheduling, or new language syntax. That scope discipline is what makes standardization plausible. A library author could write reactive state logic once against the standard primitive and have it work inside Angular, Solid, Lit, or any other framework that adopts it, instead of shipping five separate adapters. Stage 1 to Stage 3 typically takes one to three years for a TC39 proposal, so nothing about your framework’s API changes next quarter. What changes is the direction: the reactivity model most frameworks already bet on is heading toward becoming a language-level standard rather than staying a framework-specific implementation detail.
What this means if you’re choosing a framework today
Don’t pick a framework because it has “signals” in its marketing copy; by 2026 most of the mainstream options do, in one form or another. What actually differs is where the fine-grained update work happens (build time in Svelte, runtime in Solid and Preact, wrapped behind a familiar API in Vue) and how mature the ecosystem around that choice is for your team. If your app does a lot of frequent, localized updates, dashboards, live-editing tools, real-time feeds, the fine-grained model genuinely wins on update performance over React’s reconciliation approach. If your constraints are more about hiring, tooling maturity, or an existing codebase, that architectural edge is real but often smaller than the practical cost of a framework migration. Read the comparison in the context of your actual constraints, not the benchmark alone: our breakdown of React versus Angular versus Vue walks through the four questions worth asking before a framework decision, reactivity model included.
Frequently asked questions
- What is a signal in JavaScript?
- A signal is a wrapper around a value that tracks who reads it. Read a signal inside a computed value or a rendering function and that consumer is automatically registered as a dependent. Write a new value to the signal and only the registered dependents re-run, skipping everything else that didn't actually depend on that value. That targeted update is what people mean by 'fine-grained reactivity': the framework knows exactly what changed and updates exactly that, instead of re-checking a whole component tree.
- How is a signal different from React state?
- React's useState triggers a re-render of the entire component function, and React then diffs the resulting virtual DOM against the previous one to figure out what actually needs to change in the real DOM. A signal skips that diffing step. Because the framework already knows which specific pieces of the UI read a given signal, it can update those pieces directly without re-running the surrounding component or comparing trees. React's compiler-based optimizations (automatic memoization) chase a similar outcome from the opposite direction: keep the coarse-grained model, let a compiler figure out what to skip.
- Why do Svelte 5's runes work differently from Solid's signals if they're both 'signals'?
- They target the same outcome through different mechanisms. Solid's signals are runtime objects: actual function calls that track dependencies as your code executes. Svelte 5's runes are a compiler feature; the reactive behavior is analyzed and rewritten at build time, so by the time your code runs in the browser, there's no signal object left, just the direct DOM updates the compiler generated. Both give you fine-grained updates. One does it with a runtime library, the other by having the build step do the work upfront.
- What does the TC39 Signals proposal actually standardize?
- Two primitives: a writable State signal that holds a value, and a Computed signal that derives a new value from other signals and recalculates automatically when its dependencies change, all without manual subscribe or cleanup code. The proposal explicitly leaves out rendering, effect scheduling, and any new syntax, keeping the scope to the reactive data layer only. The goal is that a library or team could write that reactive layer once and have it work across any framework that adopts the standard, instead of rewriting the same dependency-tracking logic per framework.
- Should I pick a signals-based framework because it's faster?
- Raw benchmark speed is one input, not the whole decision. Solid and fine-grained-reactivity frameworks generally win update-heavy benchmarks because they skip virtual DOM diffing entirely, but React's ecosystem size, hiring pool, and tooling maturity often outweigh a benchmark gap for a given team and project. If your app does a lot of frequent, localized updates (dashboards, editors, real-time data), the fine-grained model has a real architectural edge. If your bottleneck is initial load or team ramp-up speed, framework maturity and familiarity usually matter more than the reactivity model underneath it.
Sources
Sponsored
More from this category
More from Web Development
Sponsored
Discussion
Join the conversation.
Comments are powered by GitHub Discussions. Sign in with your GitHub account to leave a comment.
Sponsored