Writing a performant client-side filter with [hidden] and trackBy

profile
Tim Deschryver
timdeschryver.dev

In this post, we take a look at the hidden HTML attribute as an alternative to the *ngIf Angular directive.

format_quote

From MDN: The hidden global attribute is a Boolean attribute indicating that the element is not yet, or is no longer, relevant. For example, it can be used to hide elements of the page that can't be used until the login process has been completed. Browsers won't render elements with the hidden attribute set.

To provide the best possible user experience, [hidden] makes client-side filters smooth, very smooth, whereas the *ngIf directive could potential block the render cycle. Although this might just take a few milliseconds it's still sensible for a user. I don't know about you, but I know that this behavior would get on my nerves if it was me who had to interact with an application that's slightly lagging behind my input.

The end result is clearly visible in the following GIF, where multiple solutions are compared next to each other. In the example, a user filters a list of 250 words based on a query - think of it as a wordle helper -. Because the Angular renderer is fast by default for elements that don't contain logic, the example uses a delay (which set to 5ms) pipe to mimic the logic of the to-be rendered element.

A list of words that's filtered when a user is typing in the searchbox

Breaking it down link

The initial cost is equal for all solutions because they all have the same amount of elements to create and render. Afterward, the solution that's using the hidden attribute in combination with the trackBy method is the clear winner.

If we think about it, the result makes a lot of sense. hidden simply hides/unhides the elements based on an attribute that is set, thus it only needs to pay the cost of instantiate the components (the delay pipe in the example) once. The other solutions are re-creating and re-rendering elements and with that is the delay pipe re-invoked, which is delaying the rendering operation. To verify this, the delay pipe is logging every invocation.

In the last few seconds of the GIF, it's clearly visible that only the hidden with trackBy implementation is the only solution that can keep up with the user input (repeatedly searching on "a" and immediately clearing it again).

format_quote

The fastest code is the code which does not run. - Robert Galanakis

hidden VS *ngIf link

The example shows that the three examples are initially equally fast, and also when the results in the list are shrinking. For example, the list starts out unfiltered and the user starts typing "karma" in the search box. Because all of the elements are already rendered, the elements that don't meet the criteratia are destroyed and removed.

The difference occurs if the opposite happens, when the results in the list are growing. In this case, the example that's using hidden has a clear advantage.
For example, when the list is queried (and only contains a few results) and the user clears the query in the search box. Now, the destroyed elements have to be re-instantiated and re-renderend for the implementations that don't use the hidden attribute. This makes all the difference.

trackBy link

Using trackBy is an essential utility method to write a performant client-side filter.

trackBy is used in an *ngFor loop to uniquely identify the objects within the collection. Angular uses this information to determine if the DOM needs to be updated, e.g. to add or remove elements from the DOM. When the object is already present in the DOM, it doesn't need to be re-created and appended to DOM, Angular simply re-uses the existing element.

That's why the difference between the examples that are using trackBy are outperforming the example that isn't. The example that isn't using trackBy creates and renders all of the components over and over again with every keystroke. Not good.

For smaller collections, this won't make a big difference but it's proving its value for each extra entry in the collection. Because the solution that's using the hidden attribute renders the entire collection, the importance of the trackBy method increases. For the other solutions trackBy won't make such a big difference because the collection that's rendered is smaller, and the new elements would still need to be re-rendered nonetheless when they're re-added to the DOM.

Code link

Delay pipe link

The delay pipe represents the logic of the element(s) that need to be shown on the screen. This logic can be on your end, 3rd party code, and the Angular code that's executed.

Component using *ngIf link

The next component iterates over the words$ collection and uses the *ngIf directive to render the words that match the query.

Component filtering results in the TypeScript code link

The next component iterates over all found words, which are filtered in the TypeScript code based on the query.

Component using hidden link

The next component iterates over the words$ collection and uses the hidden attribute to hide the words that don't match the query.

Writing tests for elements that are hidden link

It's easy to verify that elements aren't rendered because they don't appear in the DOM. This is not true for elements that are set to hidden because these elements are always present in the DOM structure. Luckily most of the testing libraries provide the functionality to check if an element is visible.

Conclusion link

The hidden attribute is a simple but powerful HTML attribute that can be set on an element to hide it from a user (and screen readers). Using the hidden attribute results in a better experience when the visibility of elements in a larger collection need to frequently be toggled. Don't just blindly use *ngIf all over the place.

Another way to make client-side rendering faster is to use a virtual scroller, for example the virtual scroller CDK implementation of Angular Material. This makes it faster because there are only a few elements of the collection are rendered. In our case the behavior of virtual scrolling wasn't desired.

The code used in this post can be found on GitHub or directly on StackBlitz.

Feel free to update this blog post on GitHub, thanks in advance!

Join My Newsletter (WIP)

Join my weekly newsletter to receive my latest blog posts and bits, directly in your inbox.

Support me

I appreciate it if you would support me if have you enjoyed this post and found it useful, thank you in advance.

Buy Me a Coffee at ko-fi.com PayPal logo

Share this post