Wenn ein Live-Feed bei jedem Tick ein vollständiges Array neu ausgibt, zwingt ein naives Ersetzen des gesamten Arrays Angulars `@for`-Block dazu, jede Zeile neu auszuwerten, selbst wenn sich nur eine geändert hat — schlimmer noch, es bricht die Referenzgleichheit für Kind-Komponenten mit `OnPush`, obwohl deren Daten identisch sind. `RowReconciler.merge` führt einen geschlüsselten Diff durch: unveränderte Zeilen behalten ihre ursprüngliche Objektreferenz (`existing`, nicht `next`), sodass `OnPush`-Kindkomponenten das Re-Rendering vollständig überspringen; nur Zeilen, deren `version` sich tatsächlich erhöht hat, werden ersetzt. In Kombination mit `track row.id` vermeidet Angulars Abgleich das Zerstören und Neuerzeugen von DOM-Knoten für Zeilen, die lediglich ihre Position gewechselt haben.
interface Row { id: string; version: number; label: string; }@Injectable({ providedIn: 'root' })export class RowReconciler {merge(current: readonly Row[], incoming: readonly Row[]): Row[] {const incomingById = new Map(incoming.map((row) => [row.id, row]));const merged: Row[] = [];for (const existing of current) {const next = incomingById.get(existing.id);if (!next) continue;merged.push(next.version === existing.version ? existing : next);incomingById.delete(existing.id);}for (const remaining of incoming) {if (incomingById.has(remaining.id)) {merged.push(remaining);}}return merged;}}@Component({selector: 'app-row-list',standalone: true,template: `@for (row of rows(); track row.id) {<app-row [data]="row" />}`,})export class RowListComponent {private readonly reconciler = inject(RowReconciler);private readonly source = inject(RowFeed).stream();readonly rows = toSignal(this.source.pipe(scan((acc, incoming) => this.reconciler.merge(acc, incoming), [] as Row[])),{ initialValue: [] },);}