Simple Angular context help component or how global event listener can affect your perfomance
Imagine we need to create reasable context help component that can be easily added as an attribute to any DOM element like:
Under the hood this component should add a help icon at the end of the wrapped content. Once user clicks on this icon some help dialog should appear. The dialog should be closed once user clicks outside or hits Escape button. That’s all.
We don’t want to use any additional library but only plain Angular code. So, let’s do that.
To achieve this functionality, we’ll be using <ng-content>
in order to preserve wrapped content. Also, we will create additional container that will contain icon and dialog itself.
Here’s complete template:
Decorators are heavily used in Angular world so we tend to use them everywhere. We’ll be utilizing @HostListener
for our purpose. Noting hard should be here and I bet you already did something similar:
Everything should work seamlessly unless we face some performance issue.
What can be the problem here?
Maybe we won’t face this issue if we have only several instantiated ContextHelp components. But in real case we put 100 and more such attributes on a page.
First thing that I usually do when profiling Angular application is put logpoint inside ApplicationRef.tick()
method:
Now, I want to remind you that each event listener that was registered within Angular Zone will trigger change detection in Angular tree view.
What we have right now?
We utilized two HostListeners
that are listening to global events. Each time user clicks on document or hits Escape button Angular will trigger change detection. The more instances of ContextHelpComponent
we have the more change detection cycles Angular will execute.
Here’s what happens if we have added 100 context-help
attributes on a page.
I hope you utilize onPush change detection in many parts of your application otherwise you will be in trouble 😉
Solutions 🎉
In order to remedy this behavior and reduce change detection cycles we can think of many options:
- coalescing events feature
With this feature enabled
Angular will defer change detection cycle by using requestAnimationFrame. As a result we will have only one tick
.
You can read more about this feature in this great article by Netanel Basal https://netbasal.com/reduce-change-detection-cycles-with-event-coalescing-in-angular-c4037199859f)
- reusable clickOutside directive which utilizes the same pair of
@HostListener
s
This way we defer subscription to those events until dialog appears in the DOM.
- remove
@HostListener
s from ts code and move subscriptions to template by leveraging (output) events on dialog:
This behave similar to previous option. Angular won’t subscribe to global listener until dialog appears in the DOM.
Thank you for reading! Keep coding!