This week I got to work on a project that had some difficulties to render an Angular template efficiently. During a (re)render, the screen froze and became unresponsive until the render cycle settled. The template itself wasn't too fancy, it's just a couple of CRUD tables with tabular data to show the working schedule of an employer on a monthly basis. Each table (a week) does have between 10 and 30 rows, in total, this means that there are between 50 and 150 rows on the screen.
While the code wasn't optimized, I was a bit surprised that Angular has difficulties rendering the view. That's why I send out a tweet with my recent experience. Some helpful friends responded to my tweet with improvements to solve this rendering problem.
All of the suggestions are valid (and easy to make) tweaks to reduce the number of change detection cycles, which is the underlying problem. For example:
- to use the
OnPushstrategy instead of the
- to use pure pipes to format properties to a human-readable text, to prevent extra method invocations;
- to use the
trackBymethod, to prevent rows to be re-rendered in a
- to use a virtual scroller, to only show a few rows at a time;
But to solve the problem, I went with a different route which led me to success before.
I like to extract most (or all) of the logic outside of the component/template, to prepare a model before it reaches the component. This doesn't require you to know about specific Angular APIs, and it keeps the component small and clean. I also find this easier to test, debug, and to possibly change the behavior in the future.
To get an understanding of what I mean by saying "preparing the model", let's first take a look at the code that was causing problems.
If you're an experienced Angular developer, I'm sure that you can spot the red flags in the code that we just saw. To get everyone on the same page, the main problem is that there are a lot of methods that are used inside of the template. While this is probably bearably noticeable at first, it can become a problem when the logic inside these methods gets more expensive. For every change detection cycle, all of the methods are executed. This means that a single method can be invoked multiple times before a render cycle has been completed.
Now that we know the cause of the problem, we also know why we need to do our absolute best to reduce the number of change detection cycles and why it's important to keep methods in a template to a bare minimum.
Instead of using the proposed fixes, let's take a look at the solution if the data is pre-processed.
By looking at the template and the code, we notice that there's logic to build up the template. For example, the two heaviest methods are a method to concat two collections before sorting them, and the second-heaviest method is to only display the unique messages. Besides those, there were also a handful of simpler methods, for example, to format multiple properties, or to show/hide a button.
If we move all of this view logic to outside the component, these methods are only invoked once, instead of with each change detection cycle.
The application that I'm working on uses NgRx, which has the concept of selectors. To me, selectors are the ideal location to move the view logic to. Don't worry if you're not using NgRx, this technique is also applicable to other state management tools, with just pure RxJS, and even across different frameworks.
With the above selector, I find it easier to see what's going on and to spot possible mistakes. You can also see how much simpler the component gets to be after this refactor. There's no logic anymore in the component, the template just loops over the collections and uses the properties of the (view)model. Nice and simple.
Besides that it's easier to read, you also don't have to worry about the Angular change detection mechanism. The logic inside the selector is only executed when the data changes, not on every change detection cycle. This makes it very efficient.
Another advantage of this technique is that it's straightforward to test.
To test the selector, I use the
projector method on the selector.
projector exists for exactly this reason, making it easy for us to test the logic inside the selector.
With it, we can call the selector with fixed variables, and then we assert the result of the selector.
This is faster to execute and to write, compared to writing and running a component test.
When you do this and the view is still on the slow end, you can still resort to the Angular optimization techniques that were mentioned earlier. From my experience, for the applications that I create, this "fix" is usually sufficient, but it's always good to know that you have an extra pair of tricks in your bag.
- Complex logic doesn't belong in the template nor in the component
ngIfonly as terminal operations (from a precomputed dataset)
- Abstract the selection logic using selectors if you're using NgRx
I appreciate it if you would support me if have you enjoyed this post and found it useful, thank you in advance.