Angular Performant List Rendering
As an Angular developer, no doubt, you use *ngFor
many times every day. There are a couple of things we can do to squeeze more performance out of Angular when rendering our list-like component items.
TLDR;
- Always use
trackBy
. - Check out
@rx-angular/cdk
and*rxFor
to reduce frame drops.
The first, and yet often overlooked way to improve list rendering performance is the thetrackBy
function. Specify the function in the template by setting the trackBy
property:
<li *ngFor="let item of items; trackBy: trackByItem">...</li>
In the component we provide the trackByItem
function which has the (simplified) signature index: number, item: any
:
trackByItem(index: number, item: Item) {
return item.id;
}
// or
trackByItem(index: number, item: Item) => item.id
You want to return whatever determines an object’s uniqueness. Usually, this will be an ID of some sort, or can be a combination of fields. Just ensure that whatever you return is truly unique. Angular will use this to ensure that the item in the DOM that is getting manipulated is the correct item based on this return value. This prevents Angular from having to traverse through all items when changes occur in the source items
.
This is really beneficial when you have a large rendered list and then are adding, removing, or reordering the items in the list in some way.
But, wait! There’s more!
Due to the amount of work that may be required to render your list, you could find that you’re still getting frame drops during rendering.
Wait!? What’s a frame drop? Browsers have a 50–60ms “frame” in which scripts can cause the layout to change. Style changes can cause “forced synchronous layout”. From developers.google.com:
Forced synchronous layout occurs when the browser runs layout inside a script, and then does something that forces it to recalculate styles, thus requiring it to run layout again. This typically happens inside a loop which iterates through an array of divs and adjusts their properties, causing forced synchronous layout.
If the changes take longer than the 50–60ms frame to process and render, the current frame is dropped (not rendered) and the browser attempts to render the next frame. If you’re doing animations that means that you skip steps in the animation, resulting in a stutter in the animation.
Imagine the use case of clicking on a tab control that has animation on the tabs to highlight the newly selected tab. But the tab loads and renders a large list. Because the rendering of a large list of complex components is almost certain to cause dropped frames, you’ll likely see that the browser is not rendering the smooth animation in the tab control because the list rendering is blocking. Blocking in the browser makes your app feel sluggish and unpolished, and it can frustrate a user when clicking on something doesn’t feel like it responded.
You can see where your application is dropping frames by opening Chrome DevTools and going to the Performance tab. Get your app to the point in question, then click record in DevTools and perform your action. Here is what you will see when done in DevTools (this is from rendering this blog article):
Everything in red is where your app is dropping frames. You can scroll in/out to get more detail in the lower section. You can see that Layout Shift is happening, causing forced synchronous layout.
Now, there are many things that could cause this behavior. We’re only going to focus on what we can do to help reduce the possibilities of this as it relates to *ngFor
and our lists of components.
There are two packages directly focused on helping to improve Angular rendering performance. These allow you to tackle many of the causes of rendering performance besides just the lists. But we’ll keep focused on the list.
npm install @rx-angular/cdk
will give us access to a new structural directive *rxFor
. Here is a quick comparison of use of *ngFor
and *rxFor
:
<li *ngFor="let item of items$ | async; trackBy: trackByFn">...</li>
<li *rxFor="let item of items$; trackBy: trackByFn">...</li>
In many c,ases this simple change can, in some cases, drastically improve the number of dropped frames when dealing with lists. Notice that we no longer need the async
pipe either! Simpler, cleaner code, that performs better!
The folks at @rx-angular have provided a sample application that shows the frame dropping in *ngFor
vs the clean rendering with *rxFor
here:
Clicking from the empty tab to the native tab will show the frame blocking. You can see that the animations on the tabs is stuttering. If you go from empty tab to the concurrent tab the animation will be clean and polished.
This may just be a quick fix that polishes your UI to give it that much more appeal to your users!
Want to optimize more? Be sure and check out @rx-angular/template
and how to use *rxLet
to avoid using the *ngIf
hack to get an observable into a variable. And check out all of @rx-angular
for their many optimizations.
Now that you’ve read this article and learned a thing or two (or ten!), let’s kick things up another notch!
Take your skills to a whole new level by joining us in person for the world’s first MAJOR Angular conference in over 2 years! Not only will You be hearing from some of the industry’s foremost experts in Angular (including the Angular team themselves!), but you’ll also get access to:
- Expert panels and Q&A sessions with the speakers
- A friendly Hallway Track where you can network with 1,500 of your fellow Angular developers, sponsors, and speakers alike.
- Hands-on workshops
- Games, prizes, live entertainment, and be able to engage with them and a party you’ll never forget
We’ll see you there this August 29th-Sept 2nd, 2022. Online-only tickets are available as well.