Ein Computed Signal ist ein Read-only-Signal, dessen Wert aus anderen Signals abgeleitet wird. Du beschreibst die Ableitung als reine Funktion, Angular erledigt den Rest: Es trackt automatisch, welche Signals du gelesen hast, cached das Ergebnis, evaluiert nur bei Bedarf und garantiert, dass abhängige Komponenten zwischenzeitlich nie inkonsistente Daten sehen. Mit Angular 21 (aktueller Stand dieses Artikels) ist computed() der idiomatische Weg, abgeleiteten State im Template oder zwischen Services bereitzustellen — kürzer, performanter und sicherer als Getter, Methoden im Template oder klassische RxJS-combineLatest-Pipelines. Dieser Artikel zeigt dir Lazy-Evaluation, Glitch-free-Semantik, Custom-Equality, Verkettung und ein realistisches Filter-Beispiel.

Read-only, lazy, memoized

computed() nimmt eine Ableitungs-Funktion und gibt einen Signal<T> zurück — einen reinen Lese-Signal, kein WritableSignal. Du kannst keinen .set()- oder .update()-Aufruf darauf machen; der TypeScript-Compiler verbietet es. Der Wert ergibt sich ausschließlich aus dem Code in der Ableitung.

Drei Eigenschaften prägen das Verhalten:

  • Lazy. Die Ableitungsfunktion läuft erst, wenn jemand das Signal zum ersten Mal liest. Solange niemand fragt, wird nichts berechnet.
  • Memoized. Beim Lesen prüft Angular, ob sich seit dem letzten Lauf eine Dependency geändert hat. Wenn nicht, kommt der gecachte Wert zurück, ohne dass die Funktion erneut läuft.
  • Glitch-free. Auch wenn mehrere Source-Signals nacheinander gesetzt werden, läuft computed höchstens einmal pro Read — und nie mit einem inkonsistenten Zwischen-State.

Hello World: voller Name aus Vor- und Nachname

Das kleinste sinnvolle Beispiel: Zwei signal()-Quellen, ein computed(), das beide kombiniert.

TypeScript full-name.component.ts
import { Component, signal, computed } from '@angular/core';

@Component({
    selector: 'app-full-name',
    standalone: true,
    template: `
        <input [value]="firstName()" (input)="firstName.set($any($event.target).value)" />
        <input [value]="lastName()" (input)="lastName.set($any($event.target).value)" />

        <p>Voller Name: {{ fullName() }}</p>
    `,
})
export class FullNameComponent {
    firstName = signal('Max');
    lastName = signal('Mustermann');

    fullName = computed(() => `${this.firstName()} ${this.lastName()}`);
}

Ändert der Nutzer firstName, weiß Angular dank automatischem Dependency-Tracking, dass fullName betroffen ist. Beim nächsten Render-Pass wird fullName() erneut gelesen, die Ableitungsfunktion läuft genau einmal — und das Template zeigt den neuen Wert.

Dynamische Dependencies pro Lauf

Angular trackt nicht statisch, welche Signals im Code stehen, sondern welche Signals während eines Laufs der Funktion tatsächlich gelesen werden. Das hat Konsequenzen:

  • Ein Signal, das nur in einem nicht-genommenen if-Branch steht, ist keine Dependency.
  • Sobald die Bedingung wechselt und der andere Branch zum Zug kommt, wird die Dependency-Liste beim nächsten Lauf neu aufgebaut.
  • Du brauchst nichts manuell zu registrieren — kein subscribe, kein unsubscribe, kein takeUntil.
TypeScript conditional-tracking.component.ts
import { Component, signal, computed } from '@angular/core';

@Component({
    selector: 'app-conditional',
    standalone: true,
    template: `
        <button (click)="showAdvanced.set(!showAdvanced())">
            Modus wechseln (aktuell: {{ showAdvanced() ? 'Advanced' : 'Simple' }})
        </button>
        <p>Wert: {{ value() }}</p>
    `,
})
export class ConditionalComponent {
    showAdvanced = signal(false);
    simpleValue = signal(10);
    advancedValue = signal(99);

    // Wenn showAdvanced() === false, ist advancedValue NICHT in der Dependency-Liste.
    // advancedValue.set(123) löst dann KEIN Re-Compute aus.
    value = computed(() =>
        this.showAdvanced() ? this.advancedValue() : this.simpleValue()
    );
}

Der praktische Vorteil: Du kannst Verzweigungen ungeniert hinschreiben, ohne dass Angular bei jedem Quirk in einer ungenutzten Quelle Re-Compute macht. Die Dependency-Liste passt sich Lauf für Lauf an.

Mehrere Source-Updates, ein Re-Compute

Ein klassisches Problem reaktiver Systeme heißt Glitch: Zwei Werte, die voneinander abhängen, werden in der falschen Reihenfolge propagiert, und ein Listener sieht für einen Moment einen inkonsistenten Zustand. Angulars Signal-Graph schließt das per Konstruktion aus.

Zwischen Lese-Aufrufen ist die Welt eingefroren. Setzt du im selben synchronen Block zwei Signals, läuft das computed nicht zweimal — es läuft beim nächsten Lese-Aufruf genau einmal mit den finalen Werten beider Quellen.

TypeScript glitch-free.component.ts
import { Component, signal, computed } from '@angular/core';

@Component({
    selector: 'app-glitch-free',
    standalone: true,
    template: `<p>Summe: {{ sum() }}</p>`,
})
export class GlitchFreeComponent {
    a = signal(1);
    b = signal(2);

    sum = computed(() => {
        console.log('compute sum');
        return this.a() + this.b();
    });

    constructor() {
        // Beide Signals werden synchron aktualisiert.
        this.a.set(10);
        this.b.set(20);

        // sum() wird hier zum ersten Mal ausgewertet — und sieht
        // direkt a=10, b=20. Es gibt kein "Zwischenergebnis 12".
        console.log(this.sum()); // 30 — und 'compute sum' loggt nur einmal.
    }
}

Method im Template vs. computed Signal

In klassischem Angular ist es ein häufiges Performance-Problem, dass Methoden im Template bei jedem Change-Detection-Cycle erneut laufen — auch wenn sich keine relevante Eingabe geändert hat. Bei einer Liste mit 10 000 Einträgen und einem teuren filter-Aufruf summiert sich das schnell zu spürbarem Lag.

computed() löst das automatisch: Solange keine Source-Signal-Änderung stattgefunden hat, kommt beim Lesen der gecachte Wert zurück.

TypeScript memoization-vergleich.component.ts
import { Component, signal, computed } from '@angular/core';

interface User { id: string; name: string; active: boolean; }

@Component({
    selector: 'app-memo-vergleich',
    standalone: true,
    template: `
        <!-- SCHLECHT: läuft bei JEDEM Change-Detection-Cycle erneut -->
        <p>Aktive (Methode): {{ getActiveCount() }}</p>

        <!-- GUT: läuft nur, wenn sich users() oder threshold() ändern -->
        <p>Aktive (computed): {{ activeCount() }}</p>
    `,
})
export class MemoVergleichComponent {
    users = signal<User[]>([/* ... 10 000 Einträge ... */]);
    threshold = signal(0);

    activeCount = computed(() =>
        this.users().filter((u) => u.active).length
    );

    getActiveCount(): number {
        console.log('Methode lief');
        return this.users().filter((u) => u.active).length;
    }
}

Der Unterschied wird drastisch, sobald andere Bindings auf der Komponente updaten: Ein Klick auf einen unrelated Button löst Change Detection aus, getActiveCount() läuft erneut über alle 10 000 Einträge, activeCount() liefert in O(1) den gecachten Wert.

Wann gilt das Ergebnis als „unverändert”?

computed() akzeptiert als zweites Argument ein Options-Objekt mit einem equal-Callback. Default ist Object.is, was bei Objekten und Arrays auf Referenz-Identität hinausläuft. Wenn deine Ableitung jedes Mal ein neues Array zurückgibt, das aber strukturell identisch zum vorigen ist, würden alle nachgelagerten computeds und Komponenten unnötig neu evaluieren.

Mit einer Custom-Equality-Funktion brichst du diese Kette: Liefert sie true, behandelt Angular das Ergebnis als unverändert und propagiert nichts weiter.

TypeScript custom-equality.component.ts
import { Component, signal, computed } from '@angular/core';

interface Item { id: string; label: string; }

const sameOrder = (a: Item[], b: Item[]) =>
    a.length === b.length && a.every((x, i) => x.id === b[i].id);

@Component({
    selector: 'app-custom-eq',
    standalone: true,
    template: `<p>Sortierte: {{ sorted().length }}</p>`,
})
export class CustomEqComponent {
    items = signal<Item[]>([
        { id: 'a', label: 'Alpha' },
        { id: 'b', label: 'Beta' },
    ]);

    // Wenn der neue Output dieselbe ID-Reihenfolge hat,
    // gilt er als unverändert — kein Downstream-Update.
    sorted = computed(
        () => [...this.items()].sort((a, b) => a.id.localeCompare(b.id)),
        { equal: sameOrder }
    );
}

Reactive Graph mit mehreren Levels

Ein computed darf andere computeds lesen — der Graph kann beliebig tief werden, und Angular hält ihn konsistent. Jedes Level ist lazy: Erst wenn jemand das oberste computed liest, propagieren die Reads den Graphen hinunter.

TypeScript cart.component.ts
import { Component, signal, computed } from '@angular/core';

interface CartItem { id: string; price: number; qty: number; }

@Component({
    selector: 'app-cart',
    standalone: true,
    template: `
        <p>Netto: {{ subtotal() | currency }}</p>
        <p>Steuer (19 %): {{ tax() | currency }}</p>
        <p>Gesamt: {{ total() | currency }}</p>
    `,
})
export class CartComponent {
    items = signal<CartItem[]>([
        { id: '1', price: 19.99, qty: 2 },
        { id: '2', price: 9.5, qty: 1 },
    ]);
    taxRate = signal(0.19);

    subtotal = computed(() =>
        this.items().reduce((sum, i) => sum + i.price * i.qty, 0)
    );

    tax = computed(() => this.subtotal() * this.taxRate());

    total = computed(() => this.subtotal() + this.tax());
}

Hier liest total zwei andere computeds. Wenn items sich ändert, weiß Angular: subtotal ist invalid → also auch tax und total. Der nächste Read auf total() evaluiert subtotal einmal, tax einmal, total einmal — und niemals doppelt, auch wenn beide Pfade subtotal lesen (Diamond-Problem ist gelöst).

Such- und Sortier-UI mit drei Source-Signals

Ein realistisches Setup: Eine User-Liste, ein Suchbegriff, ein Sortier-Kriterium. Die abgeleitete Liste ist ein computed, das von allen drei Quellen abhängt — und nur dann neu rechnet, wenn mindestens eine sich tatsächlich geändert hat.

TypeScript user-list.component.ts
import { Component, signal, computed } from '@angular/core';

interface User { id: string; name: string; email: string; createdAt: Date; }
type SortKey = 'name' | 'createdAt';

@Component({
    selector: 'app-user-list',
    standalone: true,
    template: `
        <input
            [value]="search()"
            (input)="search.set($any($event.target).value)"
            placeholder="Suche…"
        />
        <select
            [value]="sortBy()"
            (change)="sortBy.set($any($event.target).value)"
        >
            <option value="name">Name</option>
            <option value="createdAt">Erstellt</option>
        </select>

        <p>{{ filtered().length }} von {{ users().length }} Treffern</p>
        <ul>
            @for (u of filtered(); track u.id) {
                <li>{{ u.name }} – {{ u.email }}</li>
            } @empty {
                <li class="muted">Keine Treffer</li>
            }
        </ul>
    `,
})
export class UserListComponent {
    users = signal<User[]>([
        { id: '1', name: 'Anna', email: 'anna@example.com', createdAt: new Date('2025-01-12') },
        { id: '2', name: 'Bernd', email: 'bernd@example.com', createdAt: new Date('2024-08-03') },
        { id: '3', name: 'Clara', email: 'clara@example.com', createdAt: new Date('2025-03-21') },
    ]);
    search = signal('');
    sortBy = signal<SortKey>('name');

    filtered = computed(() => {
        const term = this.search().trim().toLowerCase();
        const key = this.sortBy();

        const filtered = term
            ? this.users().filter(
                  (u) =>
                      u.name.toLowerCase().includes(term) ||
                      u.email.toLowerCase().includes(term)
              )
            : this.users();

        return [...filtered].sort((a, b) =>
            key === 'name'
                ? a.name.localeCompare(b.name)
                : a.createdAt.getTime() - b.createdAt.getTime()
        );
    });
}

Wann was nutzen?

AspektMethode im TemplateGetter (get foo())computed() Signal
Läuft pro CD-CycleJaJaNein
MemoizationNeinNeinJa
Dependency-TrackingManuellManuellAutomatisch
Glitch-freeNeinNeinJa
Custom EqualityManuellManuell{ equal: ... }
Read-only erzwingenNeinJaJa

Faustregel: Sobald ein abgeleiteter Wert auf reactive State basiert und im Template oder in mehreren Stellen gelesen wird, ist computed() die richtige Wahl. Methoden im Template solltest du nur noch für Hilfen wie Format-Strings nutzen, bei denen der Output stets exakt eine Eingabe widerspiegelt.

Besonderheiten

computed läuft nicht auf Schedule

Anders als ein effect() oder eine Methode im Template läuft computed() nicht bei jedem Change-Detection-Cycle. Die Ableitungs-Funktion wird ausschließlich dann ausgeführt, wenn jemand das Signal liest UND seit dem letzten Lauf eine Dependency invalidiert wurde. Liest niemand, passiert nichts — egal wie oft die Quellen sich ändern.

Pure Funktion — keine Side-Effects

Die Ableitung muss rein sein. Kein console.log in Production, kein HTTP-Request, kein localStorage.setItem, kein Schreiben auf andere Signals. Side-Effects führen zu unvorhersehbarem Verhalten, weil Angular die Funktion mehrfach laufen lassen kann (Glitch-Suppression, Cache-Validation). Für Side-Effects ist effect() da.

Conditional Tracking ist dynamisch

computed(() => cond() ? a() : b()) trackt nur den genommenen Branch. Wechselt cond() auf den anderen Wert, baut Angular die Dependency-Liste beim nächsten Lauf neu auf — der bisher ungetrackte Branch wird ab dann mitgeführt, der vorher getrackte fällt raus. Du musst nichts dafür tun.

Diamond-Problem ist strukturell gelöst

Wenn zwei computeds dasselbe Source-Signal lesen und ein drittes computed beide kombiniert, evaluiert Angular die Quelle pro Read-Zyklus genau einmal. Die naive Push-Propagation in klassischen Reactive-Systemen würde hier doppelte Updates verursachen — der Signal-Graph in Angular nicht.

Cycles werden zur Runtime erkannt

Wenn ein computed sich (direkt oder indirekt) selbst liest, wirft Angular einen klaren Fehler statt in einen Stack-Overflow zu laufen. Das macht Debugging deutlich einfacher als bei klassischen Observable-Pipelines, wo zyklische Subscriptions oft erst durch Memory-Profiling auffielen.

computed kann asReadonly() ersetzen

Wenn du nur einen abgeleiteten Read-only-Wert nach außen exponieren willst, ist computed(() => this._state()) der idiomatische Weg — das Signal ist intrinsisch schreibgeschützt. WritableSignal.asReadonly() brauchst du nur, wenn du wirklich exakt denselben Wert ohne Transformation freigeben willst.

Custom Equality stoppt Downstream-Updates

Mit computed(fn, { equal: cmp }) bestimmst du, wann das Ergebnis als unverändert gilt. Liefert cmp true, propagiert Angular keine Invalidierung an abhängige computeds und Templates — eine effektive Performance-Bremse für teure Sortier- oder Filter-Pipelines, die strukturell oft dasselbe Resultat liefern.

Type-Narrowing kombiniert mit Templates

Angular 21 erlaubt instanceof und exhaustive @switch-Branches im Template. Kombinierst du das mit einem computed, dessen Rückgabetyp eine Discriminated Union ist, bekommst du sauberes Type-Narrowing pro Case-Block — die Ableitung selbst kann beliebig komplex werden, das Template bleibt typsicher.

Weiterführende Ressourcen

Externe Quellen

/ Weiter

Zurück zu Signals

Zur Übersicht