Exploring Lazy Evaluation Of Computed Signals In Angular 18
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:
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:
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 →