Skip to main content
Ben Nadel at cf.Objective() 2012 (Minneapolis, MN) with: Laura Springer
Ben Nadel at cf.Objective() 2012 (Minneapolis, MN) with: Laura Springer ( @w3bchick )

Exploring Lazy Evaluation Of Computed Signals In Angular 18

By
Published in

Yesterday, on Episode 192 of the Working Code podcast, I expressed a fear that the magic of reactivity might lead to unanticipated performance issues when a computed value relies on more than one dependency. But, this fear was purely theoretical. And, it turns out, unwarranted. Computed values in Angular 18 are lazily evaluated. Meaning, they are not computed until they are actually read. And, if they're never read, they're never computed. This post is a small exploration of these Signal timing mechanics in Angular 18.

Run this demo in my JavaScript Demos project on GitHub.

View this code in my JavaScript Demos project on GitHub.

To explore Signals, I wanted to create:

  • Some basic read/write values - three of them.
  • A computed value that is consumed - the sum of the previous three values.
  • A computed value that is never consumed.
  • An effect that logs the computed value.

First, I defined the signals in my Angular component's pseudo-constructor.

@Component({ ... })
export class AppComponent {

	public value1 = signal( 0 );
	public value2 = signal( 0 );
	public value3 = signal( 0 );
	public valueSum = computed(
		() => {

			console.log( "--> Computing new sum." );
			return ( this.value1() + this.value2() + this.value3() );

		}
	);
	public neverComputed = computed(
		() => {

			console.log( "Nothing ever calls me!" );
			return Math.random();

		}
	);

}

Then, in my component's official constructor, I registered an effect to log the computed value—the sum of the first three values:

@Component({ ... })
export class AppComponent {

	/**
	* I initialize the component.
	*/
	constructor() {

		effect(
			() => {

				console.group( "Effect()" );
				console.log( "Sum was updated:", this.valueSum() );
				console.groupEnd();

			}
		);

	}

}

But, I didn't want the effect to be the only thing that consumed the computed values. In order to better understand the timing of the effect execution, I wanted to also include an explicit consumption of the computed value. I do this in my ngOnInit() life-cycle method, which turns around and calls cycle().

This code is a bit messy looking—essentially it's just logging its execution as it goes along:

@Component({ ... })
export class AppComponent {

	/**
	* I get called once after the inputs have been bound for the first time.
	*/
	public ngOnInit() {

		console.log( "%cngOnInit() method.", "font-weight: bold" );
		this.cycle();

	}

	/**
	* I cycle the value signals.
	*/
	public cycle() {

		console.log( "%ccycle() method.", "font-weight: bold" );

		console.group( "Updating Dependencies" );
		this.value1.set( this.randRange( 1, 10 ) );
		console.log( `Just updated value1 (${ this.value1() }).` );

		this.value2.set( this.randRange( 1, 10 ) );
		console.log( `Just updated value2 (${ this.value2() }).` );

		this.value3.set( this.randRange( 1, 10 ) );
		console.log( `Just updated value3 (${ this.value3() }).` );
		console.groupEnd();

		console.group( "Accessing Computed Value" );
		console.log( "PRE: About to log sum." );
		console.log( "Current sum:", this.valueSum() );
		console.log( "POST: Sum was just logged." );
		console.groupEnd();

	}

}

Now, if I run this Angular 18 code, I get the following log output:

Console logging showing that the computed value isn't updated until it is consumed in Angular 18.

As you can see, the three read/write signals were updated in sequence without the computed value being re-processed. In fact, the valueSum computed value isn't actually re-processed until I try to log it out, after the three dependencies have been updated.

The effect callback is then processed at the end, presumably using some sort of internal scheduler? I'm not quite sure yet how the timing of the effect callbacks are orchestrated.

And, as a final note, you can see that the neverComputed callback is never logged. This is because the neverComputed value is never read; and, since Signals are lazy evaluated, there's no need for Angular to ever invoke it.

I like the idea that Signals can lead to better performance in an Angular component. And, I like the idea that we can move beyond a Zone.js-driven world. But, I'm not yet sold on all of the computed value mechanics. Right now, I'd much rather explicitly call methods that, in turn, call .set() on some read/write Signals.

I asked ChatGPT about this and it actually agrees with me:

From ChatGTP: By relying on Signals for state management but handling reactivity procedurally, you keep your code more straightforward and easier to reason about, especially for developers who are more accustomed to imperative programming paradigms.

I've lived through a lot of "clever" code in my career; and, I've learned to use it with much measure. But, we'll see how it all feels once I started building applications with Signals.

Want to use code from this post? Check out the license.

Reader Comments

Post A Comment — I'd Love To Hear From You!

Post a Comment

I believe in love. I believe in compassion. I believe in human rights. I believe that we can afford to give more of these gifts to the world around us because it costs us nothing to be decent and kind and understanding. And, I want you to know that when you land on this site, you are accepted for who you are, no matter how you identify, what truths you live, or whatever kind of goofy shit makes you feel alive! Rock on with your bad self!
Ben Nadel