Make Your Angular Directive Functionality Lazy
One of my recent tasks was to replace a legacy tooltip
from our core components library with a new one. As always, I’m not trying to reinvent the wheel. To begin this task, I first find a stable native open-source JS library that I can wrap and use.
In my case, because I work a lot with popper.js
, I found tippy.js
, which was written by the same maintainer, which is an ideal solution. tippy.js
comes with a great set of features. It can be used as a tooltip
or popover
, and is themable, fast, strongly typed, accessible, and more.
Let’s wrap it with an Angular directive:
We create the tooltip by calling the tippy
function, passing the host
element, and the content
. We also run it outside of the Angular Zone because we don’t want the events registered by tippy
to cause a change detection cycle.
Now let’s use our tooltip on a large list of 700 items:
Everything works as expected; each item shows a tooltip. But we can do better. In our case, we’ve created 700 instances of tippy
, and for each element, 4 events were added by tippy
. Which means we’ve registered 2,800 events. (700 * 4)
To visualize it, we can use the getEventListeners
method in Chrome dev-tools. getEventListeners(element)
returns the event listeners registered on the specified element:
Leaving the code this way can have implications for both memory consumption and the initial render time of the component, especially on mobile devices. Let’s think for a second. Do we really need to create a tippy
instance for an element that isn’t currently visible in the view? No, we don’t.
Let’s use the IntersectionObserver
API to defer this functionality while the element is visible on the screen. If you’re not familiar with the IntersectionObserver
API, you can read the official docs.
Let’s create an observable wrapper for IntersectionObserver
:
We’ve created an observable that emits whether the element is intersecting, which means “in the view”. We also check if the browser supports IntersectionObserver
. If it doesn’t case, we always emit true
and completes. Users who use IE deserve to suffer 😜.
Now we can use the inView
observable in our tooltip directive:
And let’s run our code again:
Great. We are creating tooltips only for elements that are currently visible in the view.
Let’s answer two questions that may come to mind.
Why don’t you use a virtual scroll? We can’t use a virtual scroll with every design we have, and Angular material still caches the template, so the instance will still be in the memory.
What about event delegation? We need to pass inputs and emits outputs, and it’s hard or impossible to achieve this technique with Angular.
Summary
We’ve learned how we can defer the functionality of directives so that our application will consume less memory and load faster. The tooltip
example described in the article is only one example of such cases. I’m sure you’ll come up with more cases.
🚀 In Case You Missed It
Here are a few of my open source projects:
- Akita: State Management Tailored-Made for JS Applications
- Spectator: A Powerful Tool to Simplify Your Angular Tests
- Transloco: The Internationalization library Angular
- Forms Manager: The Foundation for Proper Form Management in Angular
- Cashew: A flexible and straightforward library that caches HTTP requests
Follow me on Medium or Twitter to read more about Angular, Akita and JS!