Skip to content

PR #62284 (commit 3c008c9) introduced an O(N²) performance regression for computed signals that depend on many producers. #67454

@autonomobil

Description

@autonomobil

Which @angular/* package(s) are the source of the bug?

core

Is this a regression?

Yes

Description

PR #62284 (commit 3c008c9) introduced an O(N²) performance regression for computed signals that depend on many producers.

The refactor from arrays to linked lists in the reactive graph (producerNode[]producers/producersTail linked list) introduced a function isValidLink() that performs a linear scan of the entire producer linked list every time a previously-tracked signal is re-read during a recomputation.

In Angular v19 (arrays), re-reading an already-tracked producer was O(1) via indexed array access. In v20+ (linked lists), isValidLink() walks from consumer.producers (head) through the chain to find checkLink, making each re-read O(K) where K is the position in the list. This compounds to O(N²) for N producers.

The problematic code path

In producerAccessed():

const prevConsumerLink = node.consumersTail;
if (
  prevConsumerLink !== undefined &&
  prevConsumerLink.consumer === activeConsumer &&
  (!isRecomputing || isValidLink(prevConsumerLink, activeConsumer)) // ← O(N) scan
) {
  return;
}

isValidLink() walks the entire linked list from head to tail:

function isValidLink(checkLink, consumer) {
  const producersTail = consumer.producersTail;
  if (producersTail !== undefined) {
    let link = consumer.producers;  // start at HEAD
    do {
      if (link === checkLink) return true;
      if (link === producersTail) break;
      link = link.nextProducer;     // O(N) walk
    } while (link !== undefined);
  }
  return false;
}

Impact

For a computed signal with N producer dependencies, if any producer is re-read (e.g. in a second pass of a loop, or a computed that reads the same signal multiple times), the total work becomes:

$$\sum_{k=1}^{N} k = \frac{N(N+1)}{2} = O(N^2)$$

This is catastrophic for real-world applications with large collections. Our application wraps ~50,000 domain objects in individual signals and has computed signals that iterate over them. After upgrading from Angular v19 to v20+:

Metric Angular v19 (arrays) Angular v21 (linked lists)
Time to evaluate computed over 50k signals ~20ms ~10-12 seconds
Degradation factor ~500x slower
Behavior over time Stable Gets progressively worse

Critical detail: The regression only triggers on live computeds (those with downstream consumers — e.g., template bindings, effects). Non-live computeds create extra link objects during first eval, which are reused via the O(1) incremental-rebuild fast path. This is why naive benchmarks may miss the issue.

Image Image

Please provide a link to a minimal reproduction of the bug

https://stackblitz.com/edit/angular-v21-starter-zfpyrpxq?file=src%2Fapp%2Fapp.ts,package.json,README.md

Please provide the exception or error you saw

No exception — this is a performance regression, not a crash.
The application becomes unusable due to multi-second signal evaluation times.

Please provide the environment you discovered this bug in (run ng version)

Angular CLI: 21.2.0
Node: 24.13.1
Package Manager: npm 11.8.0
OS: linux x64

Angular: 21.2.0
... animations, common, compiler, compiler-cli, core, forms
... platform-browser, platform-browser-dynamic, router

Package                         Version
---------------------------------------------------------
@angular/cli                    21.2.0
@angular/core                   21.2.0
typescript                      5.8.3

Anything else?

Suggested fix: The isValidLink() function should not perform a linear scan. Possible approaches:

Replace the O(N) walk with a version number or epoch check on the link to validate it in O(1). For example, stamp each link with the consumer's current computation epoch — if they match, the link is valid.

The original PR description says the change was for "faster signal creation." However, signal creation is typically a one-time cost, while signal reads happen continuously. The tradeoff of faster creation for O(N²) reads is harmful for applications with large reactive graphs.

Related issues:

Metadata

Metadata

Assignees

No one assigned

    Labels

    area: coreIssues related to the framework runtimearea: performanceIssues related to performancecross-cutting: signalsregressionIndicates than the issue relates to something that worked in a previous versionstate: has PR

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions