Using Angular Signals for Performance Optimisation

Hero image for Using Angular Signals for Performance Optimisation. Image by Carlos Alberto Gómez Iñiguez.
Hero image for 'Using Angular Signals for Performance Optimisation.' Image by Carlos Alberto Gómez Iñiguez.

Signals can make Angular applications feel much sharper, but they are easy to oversell. They do not sprinkle speed across the whole app just because a few signal() calls appeared in the codebase. What they do offer is a more precise way to model state and derived values, which often reduces unnecessary work when the component structure is sensible.

That distinction matters. If a page feels slow because it renders a huge table, performs expensive filtering on every keypress, or keeps recomputing values in the template, signals can help. If a page feels slow because it downloads too much data or paints too much DOM, signals alone will not rescue it.

So the useful question is not "do signals improve performance?" The useful question is "what kind of work do signals help us avoid?"


Where Signals Actually Help

Signals help most when the problem is unnecessary recalculation or vague dependency flow.

In older Angular code, it is common to see templates calling methods, components manually syncing one piece of state into another, or broad observable pipelines feeding a relatively small bit of UI state. None of those patterns are automatically wrong, but they can make it harder to see what truly depends on what.

Signals improve that picture because Angular can track signal reads directly. A computed value only recalculates when one of its dependencies changes. A template that reads a signal is tied more explicitly to that state. That usually leads to less accidental work and code that is easier to reason about.


Performance Work Starts with Derived State

One of the cleanest wins is moving derived values out of template methods and into computed signals.

Consider a product list with a query box and a selected category. A common implementation is to keep the raw list in component state and then call helper methods from the template to filter and count matching items. That works, but those helpers can run far more often than people realise.

Signals give us a better place for that logic.

import {  ChangeDetectionStrategy,  Component,  computed,  signal,} from '@angular/core';type Product = {  id: number;  title: string;  category: 'books' | 'games' | 'music';};@Component({  selector: 'app-product-browser',  standalone: true,  template: `    <input      [value]="query()"      (input)="setQuery(($event.target as HTMLInputElement).value)"      placeholder="Search products"    />    <p>{{ visibleProducts().length }} results</p>    <ul>      @for (product of visibleProducts(); track product.id) {        <li>{{ product.title }}</li>      }    </ul>    `,  changeDetection: ChangeDetectionStrategy.OnPush,})export class ProductBrowserComponent {  readonly products = signal<Product[]>([]);  readonly query = signal('');  readonly category = signal<'all' | Product['category']>('all');  readonly visibleProducts = computed(() => {    const currentQuery = this.query().trim().toLowerCase();    const currentCategory = this.category();    return this.products().filter((product) => {      const matchesCategory =        currentCategory === 'all' || product.category === currentCategory;      const matchesQuery = product.title.toLowerCase().includes(currentQuery);      return matchesCategory && matchesQuery;    });  });  readonly setQuery = (value: string): void => {    this.query.set(value);  };}

The important part is not that the code uses newer APIs. The important part is that the filtering logic now has a clear reactive home. visibleProducts is cached until one of its dependencies changes. Angular does not need to rerun that filtering work merely because some unrelated event happened elsewhere in the component tree.


Why computed often beats template methods

Calling a method from a template can look harmless:

{{ getVisibleProducts().length }}

The trouble is that the method call gives Angular no cached dependency graph to work with. It is just executable code. If change detection touches the component, that method may run again.

computed gives Angular a much clearer deal. The computation is declared once, its dependencies are tracked, and the cached result can be reused until one of those dependencies changes.

This is one of the places where signals genuinely improve performance and readability at the same time. The code gets faster because the dependency flow gets more explicit.


Signals and OnPush work well together

Signals are not a replacement for OnPush. They solve related problems from different angles.

OnPush narrows when Angular checks a component subtree. Signals improve how state changes and derived values are tracked within that subtree. Used together, they usually produce a cleaner rendering model than either one on its own.

If a team adopts signals but leaves component boundaries vague, the gains are often smaller than expected. If a team uses OnPush but still relies on expensive template calls and muddled local state, that also leaves performance on the table.

The sweet spot is usually:

  • stable component boundaries
  • OnPush where the component contract is clear
  • local state in signals
  • derived state in computed

That combination tends to reduce both the number of checks and the cost of each relevant check.


Effects are Useful, but They are Not a State Management Plan

This is where signalbased code can quietly get worse before it gets better.

Effects are for imperative work such as logging, syncing to browser storage, or notifying an external service. They are not the best place to keep copying one piece of state into another just because both things change together.

This is the kind of code that ages badly:

effect(() => {  this.filteredProducts.set(    this.products().filter((product) => product.title.includes(this.query())),  );});

It works, but it turns derived state into mutable state, which means more updates, more surface area, and more opportunities to create loops or awkward timing bugs.

The better version is usually a computed:

readonly filteredProducts = computed(() =>  this.products().filter((product) => product.title.includes(this.query())),);

That change is not just stylistic. It avoids extra writes and keeps the data flow smaller. Performance work is often about that kind of restraint.


Signals Do Not Make Large DOM Problems Disappear

It is worth being blunt about this. If the component renders far too much DOM, signals will not save it.

A list of two thousand rows is still a list of two thousand rows. A component that replaces large arrays on every keystroke can still cause heavy diffing. A chart with expensive drawing logic is still expensive to draw.

Signals help most when they stop avoidable recalculation. They do not remove the cost of real rendering work. So if performance trouble sits in list size, layout thrashing, image loading, or excessive network traffic, you still need the usual fixes:

  • pagination or virtual scrolling
  • stable item tracking with @for (...; track item.id) or trackBy
  • smaller DOM trees
  • lighter templates
  • less data shipped to the browser

That is not a weakness in signals. It is just the difference between state efficiency and rendering cost.


RxJS Still Has a Job

Signals are not a reason to flatten every observable in the app into synchronous state. RxJS remains the better tool for streams, cancellation, retries, and timebased workflows.

What often works well is converting an observable into a signal at the UI boundary when the template wants the latest value directly.

For example, a route parameter stream or APIbacked store may still be modelled with RxJS, while the component uses toSignal() so that template reads and computed values remain simple. That can improve performance indirectly because it removes manual subscriptions and keeps the component logic more declarative.

The key is not choosing a winner between APIs. The key is using signals where direct state reads and cached derivation improve the component, and using RxJS where stream semantics genuinely matter.


How to Tell Whether Signals Helped

A lot of performance work goes wrong because the team changes architecture first and measures second.

If you want to know whether signals helped, look for concrete changes:

  • fewer expensive computations during typing or filtering
  • fewer template method calls
  • smoother interaction in Angular DevTools or browser performance traces
  • simpler state flow in the component itself

That last point matters more than it sounds. A component that is easier to reason about is easier to optimise further. Performance improvements are often easier to keep once the data flow has stopped fighting the framework.

The official references below are the useful ones for the APIs and rendering behaviour behind these patterns:


The Best Use of Signals is Usually the Least Dramatic One

The biggest win from signals is often not some spectacular benchmark chart. It is that components stop doing work they never needed to do in the first place.

That might mean replacing a template method with a computed value. It might mean keeping derived state derived rather than writing it back into mutable state. It might mean using OnPush and signals together so component boundaries behave more predictably.

Those are not glamorous changes, but they are the kind that hold up in production. The performance story around signals is strongest when it is treated as a story about precision. More precise dependencies. More precise recomputation. More precise component behaviour.

That is where signals earn their keep. Not by making Angular magically fast, but by helping it do less unnecessary work.


Categories:

  1. Angular
  2. Development
  3. Front‑End Development
  4. Guides
  5. JavaScript
  6. Performance