DEV Community

Cover image for ⚡ Optimizing Angular Signals with Smart Equality Checks
Romain Geffrault
Romain Geffrault

Posted on

⚡ Optimizing Angular Signals with Smart Equality Checks

Signals are a powerful tool in Angular to handle reactivity, but they can easily cause unnecessary updates, wasted requests, and performance issues if not carefully managed. In this article, we’ll explore how Signals emit updates, why equality checks (equal) matter, and how to implement efficient deep comparison strategies to keep your applications smooth and reliable.

🔄 How Signals Emit Updates

Each time the reference changes, the Signal is considered “dirty”, and all derived Signals, the DOM, and effect functions will also update.

When the signal return a string or a number, it compare the value using ===(But this article is about dealing whith object returned by the signal)

🚫 Preventing Useless Updates with equal

To avoid unnecessary Signal pipeline updates, you can use the equal option to manually define comparison logic.

This looks simple for shallow objects, but with deep objects the work becomes much heavier and harder to maintain.

📡 Angular Resource and Annoying Updates from the Source

The same issue exists with Angular Resource. You may use a Signal as a source for the params option.

This triggers an async call each time the Signal emits a new value. However, each new call cancels the previous one (similar to switchMap in RxJS).

This not only causes unnecessary requests but also removes the previous value from the Resource, which can lead to strange side effects like flaky page rendering.

⚖️ Performance of equal with Deep Nested Objects

To prevent unnecessary updates, you could add manual equality checks before calling mySignal.set(...) or mySignal.update(...).

But this is easy to forget. That’s why I prefer using the equal option:

signal({ id: 1 }, { equal: (a, b) => a.id === b.id });
Enter fullscreen mode Exit fullscreen mode

However, when dealing with deeply nested objects, equality checks can become expensive. Andrew Jarrett explored the fastest JavaScript solution for deep equals comparison, which might be helpful.

🚀 How to Make JavaScript’s Fastest Deep Equal Comparison

Here’s his article: How to build JavaScript's fastest “deep equals” function.

The solution looks like this:

const addressEquals = Function(
  "x",
  "y",
  `
  if (x === y) return true
  if (x.street1 !== y.street1) return false
  if (x.street2 !== y.street2) return false
  if (x.city !== y.city) return false
  return true
`
);
Enter fullscreen mode Exit fullscreen mode

This solution consists of wrapping your comparaison inside a string and evaluate it with the constructor of the Function.

This approach is fast but not type-safe. If your object’s shape changes, you could forget to update the comparison string.

But even if it is not typesafe, I think, it is possible to create a dedicated Type test and using a custom Type mapper to transform the object into a target string with the same shape of your object

interface MyObject {...};
const MyObjectComparaisonString = `...` as const
type IsMyDeepEqualsStringCorrect = Expect<Equal<ToDeepEqualsComparaion<MyObjectType>, typeof MyObjectComparaisonString >>
Enter fullscreen mode Exit fullscreen mode

This is just an idea, but it can be enough for your needs.

But if you already use a schema library, this lib is a perfect for your needs !

📦 @traversable: A Simple Solution for Deep Equality with Schema Validators

Andrew Jarrett also created a library that makes deep equality trivial when working with schema validators.

Using zod as an example:

import { z } from "zod";
import { zx } from "@traversable/zod";

const Address = z.object({
  street1: z.string(),
  street2: z.optional(z.string()),
  city: z.string(),
});

// Create the deep equal function 👇
const addressEquals = zx.deepEqual(Address);

addressEquals(
  { street1: "221B Baker St", city: "London" },
  { street1: "221B Baker St", city: "London" }
); // => true
Enter fullscreen mode Exit fullscreen mode

When used with a Signal:

const address = signal(..., { equal: addressEquals });
const streetViewFromAddressResource = resource({ params: address, ... });
Enter fullscreen mode Exit fullscreen mode

Now streetViewFromAddressResource is only called when the address really changes.

This works with multiple schema libraries:

  • Zod
  • JSON Schema
  • ArkType
  • TypeBox
  • Valibot

It’s a clean solution that evolves with your schema without manual updates.

Note: Be aware that if your object/schema contains functions or dates, the way they are handled may be uncertain.

👉 If you found this article useful, don’t hesitate to leave a 👍 or share your thoughts in the comments — I’d love to hear your feedback!


If you don’t know me, I’m Romain Geffrault, and I regularly share Angular/TypeScript/RxJs/Signal content. Check out my other articles and follow me on LinkedIn Romain Geffrault

Top comments (2)

Collapse
 
ahrjarrett profile image
andrew jarrett • Edited

Wow, thank you for sharing my library! This is exactly the use case I had in mind when I created it: preventing unnecessary state updates :)

Collapse
 
ahrjarrett profile image
andrew jarrett • Edited

Note: Be aware that if your object/schema contains functions or dates, the way they are handled may be uncertain.

I can answer that! Here's how zx.deepEqual handles dates and functions.

I also created a StackBlitz so your audience has something to play with.