Speeding up Angular rendering with NgRx selectors
Recently, I worked on a project where rendering an Angular template efficiently was a real struggle. Every time the view re-rendered, the browser would freeze and become unresponsive until the render cycle finished. The template itself was fairly straightforward — just a few CRUD tables displaying an employer's monthly work schedule. Each table covers one week and holds 10 to 30 rows, adding up to roughly 50 to 150 rows visible on screen at any given moment.
Even though the code wasn't particularly well-optimized, I was still surprised that Angular struggled to render the view. Curious about the cause, I posted about my experience on Twitter, and some helpful folks replied with suggestions to address the rendering issue.
Every one of those recommendations is a solid, easy-to-apply tweak aimed at cutting down the number of change detection cycles — which is what's really causing the bottleneck. A few examples:
- switching from the
Defaultto theOnPushchange detection strategy; - using pure pipes to format values into readable text instead of calling methods in the template;
- adding a
trackByfunction so rows inside a*ngFordon't get re-rendered unnecessarily; - using a virtual scroller so only a handful of rows are rendered at a time;
But for this particular problem, I chose a different path — one that has worked well for me before.
My usual approach is to push most (or even all) of the logic out of the component and the template, shaping the data into a ready-to-use model before it reaches the component. This way, you don't need to be intimately familiar with Angular's internal APIs, and the component stays lean and readable. I also find this pattern easier to test, debug, and modify later if the requirements change.
To make it clear what I mean by "preparing the model," let's first look at the code that was causing the trouble.
<div *ngFor="let message of criticalMessages().filter(onlyUnique)">{{ message }}</div>
<div *ngFor="let message of infoMessages().filter(onlyUnique)">{{ message }}</div>
<div *ngFor="let parent of parents">
<h2>{{ parent.title }}</h2>
<table>
<tr *ngFor="let child of getChildRows(parent)" [class]="getRowClass(child)">
<td><icon [icon]="getIcon(child)"></icon></td>
<td>{{ formatDate(child) }}</td>
<td [class]="getNameClass(child)">{{ formatName(child) }}</td>
<td [class]="getAddressClass(child)">{{ formatAddress(child) }}</td>
<td>{{ formatDetails(child) }}</td>
<td>
<button *ngIf="canEditChild(child)">Edit</button>
<button *ngIf="canDeleteChild(child)">Delete</button>
</td>
</tr>
</table>
</div>
@Component({})
export class Component {
// parent has a list of children
@Input() parents: Parent[];
// a message can be critical or info and is bound to a child
@Input() messages: Message[];
criticalMessages() {
return messages.filter((message) => message.type === 'critical');
}
infoMessages() {
return messages.filter((message) => message.type === 'info');
}
onlyUnique(value: Message, index: number, self: Message[]) {
return self.map((message) => message.description).indexOf(message.description) === index;
}
getChildRows(child: Child) {
const rows = child.listOne.concat(listTwo);
return rows.sort((a, b) => (a.date < b.date ? -1 : 1));
}
getIcon(child: Child) {
return this.messages
.filter((message) => message.type === 'critical')
.some((message) => message.childId === child.id)
? 'red-dot'
: '';
}
getRowClass(child: Child) {
// simple logic based on child properties
}
getNameClass(child: Child) {
// simple logic based on child properties
}
getAddressClass(child: Child) {
// simple logic based on child properties
}
canEditChild(child: Child) {
// simple logic based on child properties
}
canDeleteChild(child: Child) {
// simple logic based on child properties
}
}
If you've been working with Angular for a while, the red flags in that snippet probably jump out at you right away. The core issue is that the template is riddled with method calls. That alone might be tolerable initially, but it turns into a real problem as the work those methods perform becomes heavier. During every change detection cycle, all of those methods run — and a single method can be invoked multiple times before the render even finishes.
If this is the first time you're hearing about this, I highly recommend watching Optimizing an Angular application by Minko Gechev. It made a big difference for me early in my Angular career.
Once we understand the root cause, it's clear why minimizing change detection cycles is so important — and why keeping method calls in templates to a minimum should be a priority.
Rather than applying the fixes listed above, let's see what happens when the data is pre-processed before it gets to the view.
Looking at the original template and component, we can see that quite a bit of logic is dedicated to building up the view. The two most expensive methods are one that merges two collections and sorts the result, and another that filters down to unique messages. There were also several lighter helpers — formatting a couple of properties, or deciding whether a button should be visible.
When all of this view-building logic is moved out of the component, those methods run exactly once — instead of on every change detection pass.
Since this app is built on NgRx, I made use of selectors. For me, selectors are the perfect place to put view logic. And if you're not using NgRx, don't worry — this same idea translates to other state management libraries, plain RxJS, and even completely different frameworks.
export const selectViewModel = createSelector(
// get all the parents
selectParents,
// get all the children
selectChildren,
// get all the critical and info messages
selectMessages,
(parents, children, messages) => {
// map the child id of critical messages into a set
// this makes it easy and fast to lookup if a child has a critical message
const messagesByChildId = messages
? new Set(
messages
.filter((message) => message.type === 'critical')
.map((message) => message.childId),
)
: new Set();
// use a Set to get unique messages
const criticalMessages = messages
? [
...new Set(
messages
.filter((message) => message.type === 'critical')
.map((message) => message.description),
),
]
: [];
// use a Set to get unique messages
const infoMessages = messages
? [
...new Set(
messages
.filter((message) => message.type === 'info')
.map((message) => message.description),
),
]
: [];
return {
criticalMessages: criticalMessages,
infoMessages: infoMessages,
parents: parents.map((parent) => {
return {
title: parent.title,
children: childrenForParent(parent.listOne, parent.listTwo)
.map((child) => {
return {
id: child.id,
icon: messagesByChildId.has(child.id) ? 'red-dot' : '',
date: child.date,
state: child.confirmed ? 'confirmed' : 'pending',
edited: child.edited,
name: formatName(child),
address: formatAddress(child),
details: formatDetails(child),
canEdit: canEdit(child),
canDelete: canDelete(child),
};
})
.sort(),
};
});
};
},
);
// 💡 Tip: create a type for the view model with `ReturnType` and `typeof`
export type ViewModel = ReturnType<typeof selectViewModel>;
Using the selector above, I find it much easier to follow what's happening and catch potential mistakes at a glance. You can also see how dramatically the component shrinks after this refactor. There's no leftover logic in the component — the template simply iterates over the collections and reads properties straight from the (view)model. Nice and clean.
<div *ngFor="let message of viewModel.criticalMessages">{{ message }}</div>
<div *ngFor="let message of viewModel.infoMessages">{{ message }}</div>
<div *ngFor="let parent of viewModel.parents">
<h2>{{ parent.title }}</h2>
<table>
<tr *ngFor="let child of parent.children">
<td><icon [icon]="child.icon"></icon></td>
<td>{{ child.date }}</td>
<td [attr.state]="child.state">{{ child.name }}</td>
<td [attr.state]="child.state" [attr.edited]="child.edited">{{ child.address }}</td>
<td>{{ child.details }}</td>
<td>
<button *ngIf="child.canEdit">Edit</button>
<button *ngIf="child.canDelete">Delete</button>
</td>
</tr>
</table>
</div>
Beyond being simpler to read, you also no longer have to think about Angular's change detection at all. The selector's logic only runs when the underlying data actually changes — not during every change detection cycle. That makes it extremely efficient.
There's another nice payoff: testing this approach is a breeze.
To test the selector, I simply use its projector method. That method exists precisely for this purpose — it lets you test the selector's inner logic in isolation. You call the selector with fixed inputs and assert on the output. It's faster to write and execute than a full component test, and it's much more focused.
it('consists of unique messages', () => {
const result = selectViewModel.projector(
[{ id: 1, title: 'Parent 1' }],
[],
[
{ type: 'critical', message: 'critical message 1' },
{ type: 'critical', message: 'critical message 2' },
{ type: 'critical', message: 'critical message 1' },
{ type: 'info', message: 'info message 1' },
],
);
expect(result[0].criticalMessages).toEqual(['critical message 1', 'critical message 2']);
expect(result[0].infoMessages).toEqual(['info message 2']);
});
If you make this change and the view is still on the sluggish side, you can always fall back on the Angular-specific optimization techniques mentioned earlier. In my experience, for the apps I build, this refactor usually does the trick — but it's always reassuring to know you've got a few extra tricks up your sleeve if needed.
Follow me on Twitter at @tim_deschryver | Subscribe to the Newsletter | Originally published on timdeschryver.dev.

