Das reaktive Paradigma als Browser-Standard
Reaktive Programmierung ist in jedem modernen Frontend-Framework anders gelöst – mit inkompatiblen APIs und Abstraktion hinter Framework-Grenzen. Das TC39 Signals-Proposal definiert erstmals eine universelle Grundlage: Signal, computed und effect als native JavaScript-Primitive, auf denen Frameworks aufbauen können.
Inhaltsverzeichnis
- 1. Das Problem reaktiver Programmierung in JavaScript
- 2. Was ein Signal ist – und was es nicht ist
- 3. computed(): abgeleitete Zustände ohne manuelles Tracking
- 4. effect(): Seiteneffekte reaktiv steuern
- 5. Lazy Evaluation und Glitch-Free Semantik
- 6. Die TC39-Proposal-API im Detail
- 7. Signals in Solid.js, Angular und Vue
- 8. Interoperabilität: Framework-übergreifende Signals
- 9. Signals vs. andere Reaktivitätsmodelle
- 10. Zusammenfassung
- 11. FAQ
1. Das Problem reaktiver Programmierung in JavaScript
Reaktive Programmierung beschreibt ein Modell, in dem Zustände automatisch propagiert werden: Wenn sich ein Wert ändert, aktualisieren sich alle abhängigen Berechnungen und Seiteneffekte ohne manuelles Eingreifen. Dieses Prinzip ist in der UI-Entwicklung unverzichtbar – wenn der Nutzer seinen Namen eingibt, soll der Begrüßungstext sofort aktualisiert werden, ohne dass eine Event-Handler-Kaskade manuell verwaltet werden muss. Das Problem: Jedes Framework löst dieses grundlegende Problem anders. React nutzt useState und den Virtual-DOM-Diff-Algorithmus, Vue nutzt ein Proxy-basiertes reaktives System, Angular Signals (neu) und RxJS Observables, Svelte kompiliert Reaktivität zur Build-Zeit weg.
JavaScript Signals adressieren diese Fragmentierung auf Sprachebene. Das TC39 Signals-Proposal definiert eine minimale, aber vollständige API, die die Grundlage für Framework-Reaktivität bildet, ohne ein konkretes Framework-Modell vorzuschreiben. Die Idee: Frameworks wie Solid.js, Preact Signals, Angular und Vue sollen ihre Reaktivitäts-Layer auf native Signals aufbauen können. Bibliotheken und Web Components können dann dieselben Signal-Primitive nutzen, ohne an ein Framework gebunden zu sein. Das wäre eine Interoperabilität, die es heute nicht gibt.
2. Was ein Signal ist – und was es nicht ist
Ein Signal ist ein reaktiver Datenbehälter. Er speichert einen Wert, verfolgt automatisch alle Stellen im Code, die diesen Wert lesen, und benachrichtigt diese Abhängigkeiten, wenn der Wert sich ändert. Das ist der Unterschied zu einer normalen Variable: Eine Variable ist passiv – nichts passiert automatisch, wenn sie sich ändert. Ein Signal ist aktiv – es führt ein Abhängigkeits-Tracking durch und propagiert Änderungen durch das reaktive System.
Was ein Signal nicht ist: Es ist kein Observable wie in RxJS. Observables sind Push-basiert und beschreiben Ereignisströme über die Zeit. Signals sind Pull-basiert mit Lazy Evaluation – der aktuelle Wert wird immer synchron gelesen, und das reaktive System aktualisiert Abhängigkeiten bei Bedarf. Es ist auch kein Store wie in Redux oder Zustand: Ein Store ist eine externe Zustandsquelle mit expliziten Aktionen und Reduzierfunktionen. Ein Signal ist ein einfacher Wertbehälter, der Reaktivität durch implizites Tracking statt durch explizites Publish-Subscribe realisiert. Diese Einfachheit ist der Kern des Signal-Designs.
// TC39 Signals Proposal — Signal.State and Signal.Computed
// (Polyfill: npm install signal-polyfill)
import { Signal } from 'signal-polyfill';
// Signal.State: writable reactive value
const count = new Signal.State(0);
const name = new Signal.State('World');
// Reading the current value
console.log(count.get()); // 0
// Writing a new value — automatically notifies dependents
count.set(1);
count.set(count.get() + 1); // increment
// Signal.Computed: derived reactive value (lazy + cached)
const greeting = new Signal.Computed(() => `Hello, ${name.get()}!`);
const doubled = new Signal.Computed(() => count.get() * 2);
// computed is lazy — only evaluated when actually read
console.log(greeting.get()); // 'Hello, World!'
name.set('Mironsoft');
// greeting is now stale — but NOT yet re-evaluated
console.log(greeting.get()); // 'Hello, Mironsoft!' — evaluated on demand
console.log(doubled.get()); // 4 (count is 2)
3. computed(): abgeleitete Zustände ohne manuelles Tracking
Ein computed Signal berechnet seinen Wert aus anderen Signals heraus. Der entscheidende Punkt: Das Abhängigkeits-Tracking ist vollständig automatisch. Der Entwickler muss nicht deklarieren, von welchen Signals ein computed abhängt – das reaktive System ermittelt die Abhängigkeiten dynamisch beim ersten Durchlauf der Berechnungsfunktion. Jeder Signal.get()-Aufruf innerhalb der computed-Funktion registriert automatisch eine Abhängigkeit. Wenn eines dieser Quell-Signals später aktualisiert wird, markiert das System das computed Signal als stale.
Das Elegante an computed Signals ist die automatische Kaskadierung: Ein computed Signal kann andere computed Signals lesen, die wiederum andere computed Signals lesen. Das System baut einen Abhängigkeitsgraphen auf und propagiert Änderungen korrekt durch alle Ebenen. Dabei ist das Ergebnis eines computed Signals gecacht: Es wird nicht bei jeder Änderung einer Quelle sofort neu berechnet, sondern nur wenn es tatsächlich gelesen wird. Diese Lazy Evaluation ist der Schlüssel zur Performance-Effizienz des Signal-Modells.
4. effect(): Seiteneffekte reaktiv steuern
Ein Effect ist der Mechanismus, mit dem reaktive Signals die reale Welt beeinflussen – DOM-Updates, Netzwerkanfragen, Logging, LocalStorage-Writes. Das Proposal definiert keinen eingebauten Effect-Primitive (das ist absichtlich), bietet aber das Werkzeug, um einen zu bauen: Signal.subtle.Watcher. Ein Watcher registriert sich bei Signals und wird benachrichtigt, wenn eines der beobachteten Signals als stale markiert wird. Frameworks nutzen diese Low-Level-API, um ihre eigenen Effect-Implementierungen zu bauen.
Der Grund, warum das Proposal keinen fertigen effect() einbaut: Effects haben unterschiedliche Semantiken in verschiedenen Frameworks. Wann läuft ein Effect – synchron, asynchron, in einem Microtask? Wie wird Cleanup gehandhabt? Wie verhält sich ein Effect, wenn er während seiner Ausführung eine Exception wirft? Das Proposal lässt diese Fragen bewusst offen und stellt mit Signal.subtle.Watcher nur das Werkzeug bereit. Frameworks wie Solid.js, Preact Signals und Angular können unterschiedliche Effect-Semantiken implementieren, alle aufgebaut auf demselben Signal-Fundament.
// Building an effect() on top of Signal.subtle.Watcher
import { Signal } from 'signal-polyfill';
// Simple effect implementation using the Watcher primitive
function effect(fn) {
let cleanup;
const watcher = new Signal.subtle.Watcher(() => {
// Called synchronously when a dependency becomes stale
// Schedule re-execution asynchronously to batch updates
queueMicrotask(run);
});
function run() {
// Stop watching previous dependencies
watcher.unwatch(...watcher.getPending());
// Clear previous cleanup
if (cleanup) cleanup();
// Re-execute fn, tracking all signal reads
watcher.watch(computed);
cleanup = Signal.subtle.untrack(() => fn());
}
const computed = new Signal.Computed(fn);
watcher.watch(computed);
run(); // Execute immediately
// Return disposal function
return () => {
watcher.unwatch(computed);
if (cleanup) cleanup();
};
}
// Usage
const temperature = new Signal.State(20);
const unit = new Signal.State('C');
const display = new Signal.Computed(() =>
unit.get() === 'C'
? `${temperature.get()}°C`
: `${(temperature.get() * 9/5 + 32).toFixed(1)}°F`
);
// Effect runs immediately and on every dependency change
const dispose = effect(() => {
document.title = `Temperatur: ${display.get()}`;
});
temperature.set(25); // title updates to "Temperatur: 25°C"
unit.set('F'); // title updates to "Temperatur: 77.0°F"
dispose(); // stop the effect
5. Lazy Evaluation und Glitch-Free Semantik
Zwei Konzepte sind zentral für das Verständnis von JavaScript Signals: Lazy Evaluation und Glitch-Free Semantik. Lazy Evaluation bedeutet, dass computed Signals ihren Wert nicht sofort berechnen, wenn eine Abhängigkeit sich ändert. Stattdessen werden sie als stale markiert. Die Neuberechnung findet erst statt, wenn der Wert tatsächlich gelesen wird. Das ist der fundamentale Unterschied zu einem Push-basierten System wie RxJS, das sofort neue Werte durch alle Subscriptions propagiert.
Glitch-Free Semantik bezeichnet die Eigenschaft, dass Konsumenten eines reaktiven Systems niemals einen inkonsistenten Zustand sehen. Ein "Glitch" in reaktiven Systemen entsteht, wenn ein computed Signal gelesen wird, bevor alle seine Abhängigkeiten aktualisiert wurden. Beispiel: Wenn a sich ändert und ein computed b von a abhängt und ein anderes computed c sowohl von a als auch von b abhängt, könnte ein naives System c mit einem alten b und dem neuen a berechnen – inkonsistent. Das Signal-Proposal garantiert glitch-free Semantik durch den topologischen Sortiermechanismus des Abhängigkeitsgraphen.
6. Die TC39-Proposal-API im Detail
Das Proposal definiert die Signal-API unter dem Namespace Signal. Signal.State ist der beschreibbare Basis-Signal-Typ mit den Methoden get() und set(value). Signal.Computed ist der abgeleitete, read-only Signal-Typ mit nur get(). Der Konstruktor nimmt eine Berechnungsfunktion und optionale Options-Parameter an, darunter einen Custom Equality Check, der entscheidet, ob eine Änderung des berechneten Wertes Abhängigkeiten benachrichtigt.
Unter Signal.subtle befindet sich die Low-Level-API für Framework-Autoren: Signal.subtle.Watcher ist der Mechanismus, um auf Stale-Notifications zu reagieren. Signal.subtle.untrack(fn) führt eine Funktion aus, ohne dabei Abhängigkeiten zu tracken – nützlich innerhalb von Effects, um auf Signal-Werte zuzugreifen ohne sie als Abhängigkeit zu registrieren. Signal.subtle.currentlyTracking() gibt das aktuell laufende computed zurück, was Framework-Autoren für Debugging-Tools nutzen können. Diese API-Trennung zwischen der Endnutzer-API und der Framework-API ist eine bewusste Designentscheidung des Signals-Proposals.
// Advanced Signal patterns — custom equality, untrack, batch
import { Signal } from 'signal-polyfill';
// Custom equality: only notify dependents when reference changes
const userProfile = new Signal.State(
{ name: 'Alice', role: 'admin' },
{
equals: (prev, next) =>
prev.name === next.name && prev.role === next.role,
}
);
// Reading without tracking — useful inside effects for one-time reads
const currentCount = new Signal.State(0);
const isExpensive = new Signal.Computed(() => {
// Access currentCount without creating a dependency
const snapshot = Signal.subtle.untrack(() => currentCount.get());
return snapshot > 1000;
});
// Composition: signals built from other signals
const firstName = new Signal.State('Ada');
const lastName = new Signal.State('Lovelace');
const fullName = new Signal.Computed(() => `${firstName.get()} ${lastName.get()}`);
const slug = new Signal.Computed(() =>
fullName.get().toLowerCase().replace(/\s+/g, '-')
);
// slug depends on fullName which depends on firstName and lastName
// When firstName changes, slug is re-evaluated automatically
firstName.set('Grace');
console.log(slug.get()); // 'grace-lovelace'
7. Signals in Solid.js, Angular und Vue
Das TC39 Signals-Proposal ist stark von bestehenden Framework-Implementierungen inspiriert. Solid.js nutzt seit seiner Entstehung ein Signal-basiertes Reaktivitätsmodell: createSignal() gibt ein State-Signal zurück, createMemo() entspricht computed und createEffect() entspricht effect. Solid.js gilt als direkter Vorläufer des Proposals, weil es bewiesen hat, dass Signal-basierte Reaktivität performanter als VDOM-basierte Ansätze sein kann. Die Solid.js-Implementierung ist so effizient, dass sie bei vielen Benchmarks React übertrifft.
Angular führte ab Version 16 ein eigenes Signals-Modell ein, das eng an das TC39-Proposal angelehnt ist. Angular-Signals sind mit signal(), computed() und effect() zugänglich – Namen, die absichtlich an das Proposal angelehnt sind. Vue 3's Reaktivitätssystem mit ref(), computed() und watchEffect() implementiert dasselbe Grundprinzip unter anderen Namen. Das Ziel des TC39-Proposals ist es, dass diese Frameworks ihre internen Reaktivitäts-Primitive auf native JavaScript Signals migrieren können, sobald das Proposal Stage 4 erreicht. Das würde Framework-übergreifende Interoperabilität von reaktiven Daten ermöglichen.
8. Interoperabilität: Framework-übergreifende Signals
Die wichtigste langfristige Auswirkung des JavaScript Signals-Proposals ist Interoperabilität. Heute ist ein Signal-Wert aus einem Solid.js-Kontext für eine Vue-Komponente unsichtbar und umgekehrt. Web Components haben kein standardisiertes reaktives System. Wenn native Signals im Browser verfügbar sind, können Web Components Signal-Werte lesen und selbst Signals exportieren. Eine Angular-Komponente könnte ein Signal von einer Solid.js-Bibliothek konsumieren, ohne Adapter oder Wrapper zu benötigen.
Diese Interoperabilität ist besonders wertvoll für den Aufbau von Design-System-Bibliotheken und Web Components, die framework-agnostisch sein sollen. Statt für jedes Framework eine separate Wrapper-Bibliothek zu pflegen, könnte ein Web Component ein natives Signal als Property akzeptieren und exponieren. Der Framework-spezifische Code liegt dann ausschließlich in der Komponenten-Integration des jeweiligen Frameworks. Das würde die Fragmentierung des Frontend-Ökosystems erheblich reduzieren – eines der ambitioniertesten Ziele des TC39 Signals-Proposals.
9. Signals vs. andere Reaktivitätsmodelle
Das Verständnis der Unterschiede zwischen JavaScript Signals und anderen Reaktivitätsmodellen ist entscheidend für die richtige Anwendung. Signals sind pull-basiert mit lazy evaluation. RxJS Observables sind push-basiert und eignen sich besonders für asynchrone Ereignisströme mit komplexen Transformationen. React State mit useReducer modelliert Zustandsänderungen als diskrete Aktionen in einem Zustandsautomaten. Redux verstärkt dieses Muster mit einem Single-Source-of-Truth-Store.
| Modell | Evaluierungs-Strategie | Abhängigkeits-Tracking | Stärke |
|---|---|---|---|
| JS Signals (TC39) | Pull, lazy, gecacht | Automatisch, implizit | Glitch-free, interoperabel |
| RxJS Observables | Push, sofort | Explizit (subscribe) | Asynchrone Ströme, Operatoren |
| React useState | Push, Re-render-basiert | Implizit (VDOM-Diff) | Einfaches Modell, großes Ökosystem |
| Vue ref/computed | Pull, lazy, gecacht | Automatisch (Proxy) | Einfache API, Proxy-basiert |
| Redux / Zustand | Push, selektor-basiert | Explizit (Selektoren) | Vorhersehbar, time-travel-fähig |
JavaScript Signals sind nicht als Ersatz für RxJS gedacht. Sie lösen unterschiedliche Probleme: Signals modellieren synchronen, abgeleiteten Zustand – "Was ist der aktuelle Wert dieser Berechnung?" Observables modellieren Ereignisströme über die Zeit – "Was passiert als Nächstes in diesem Stream?" In einer Angular-Anwendung ergänzen Signals und RxJS sich: Signals für UI-Zustand, Observables für HTTP-Anfragen und komplexe Ereignis-Transformationen.
Mironsoft
Modernes Frontend, reaktive Architekturen und performante JavaScript-Anwendungen
Reaktive Architektur für Ihre Anwendung?
Wir beraten und implementieren reaktive Zustandsmodelle mit Signals, Solid.js, Angular oder Vue – abgestimmt auf die Anforderungen Ihrer Anwendung und Ihres Teams.
Architektur-Beratung
Reaktivitätsmodell wählen: Signals, Observables oder Store – mit konkreter Empfehlung für Ihr Projekt
Migration
Angular-Signals-Migration, Solid.js-Einführung oder Vue-Composables-Modernisierung
Performance-Optimierung
Rendering-Bottlenecks mit Signals eliminieren und unnötige Re-renders messbar reduzieren
10. Zusammenfassung
Das JavaScript Signals-Proposal bringt reaktive Programmierung als standardisiertes Primitive in die Sprache. Signal.State für beschreibbare Zustände, Signal.Computed für abgeleitete Berechnungen mit automatischem Abhängigkeits-Tracking und Signal.subtle.Watcher für Effects – zusammen bilden diese drei Primitive eine vollständige Grundlage für reaktive UI-Architektur. Lazy Evaluation und Glitch-Free Semantik sind die technischen Garantien, die das Modell korrekt und effizient machen.
Die langfristige Bedeutung von nativen JavaScript Signals liegt in der Framework-übergreifenden Interoperabilität. Wenn Solid.js, Angular, Vue und Web Components auf dasselbe Signal-Fundament bauen, können reaktive Daten fließend zwischen Framework-Grenzen übergehen. Das TC39-Proposal befindet sich derzeit in Stage 2; die finale Implementierung in Browsern ist noch einige Jahre entfernt. Mit dem Polyfill signal-polyfill lassen sich Signals und der Signal.subtle.Watcher-Mechanismus aber bereits heute in Produktionsprojekten erkunden.
JavaScript Signals — Das Wichtigste auf einen Blick
Primitive
Signal.State (beschreibbar), Signal.Computed (abgeleitet, lazy), Signal.subtle.Watcher (für effects). Drei Primitive ergeben ein vollständiges reaktives System.
Tracking
Automatisches Abhängigkeits-Tracking ohne manuelle Deklaration. Jeder get()-Aufruf in einem computed registriert eine Abhängigkeit implizit.
Lazy + Glitch-Free
computed Signals werden erst bei Lesezugriff neu berechnet. Glitch-Free garantiert topologisch konsistente Auswertung ohne inkonsistente Zwischenzustände.
Status
TC39 Stage 2. Polyfill: signal-polyfill auf npm. Solid.js, Angular Signals und Vue ref() sind Framework-Vorläufer desselben Paradigmas.