Make Your Angular Directive Functionality Lazy

Netanel Basal
Netanel Basal
Published in
3 min readApr 28, 2020

--

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:

Aggregates the total events count

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!

--

--

A FrontEnd Tech Lead, blogger, and open source maintainer. The founder of ngneat, husband and father.