In this exercise we will focus on basic runtime optimizations in Angular application.
First we create a component that will help us to debug change detection cycles in our application.
Your task is to create a DirtyChecksComponent
which should serve as a performance debug utility.
Whenever the application renders, it should increase a number in its template.
To do so, the component should bind a renders()
function inside of the template which
simply increase a local numeric variable everytime getting called.
For the template use a <code>
tag with the class .dirty-checks
.
hint for the template
<code class="dirty-checks">({{ renders() }})</code>
Afterwards, use the component in the AppComponents
template and serve the application.
show solution
Go to shared
folder. Create dirty-checks.component.ts
file with following content:
import { Component, ElementRef, NgModule } from "@angular/core";
@Component({
selector: "dirty-checks",
template: ` <code class="dirty-checks">({{ renders() }})</code> `,
})
export class DirtyChecksComponent {
private _renders = 0;
renders() {
return ++this._renders;
}
}
@NgModule({
declarations: [DirtyChecksComponent],
exports: [DirtyChecksComponent],
})
export class DirtyChecksComponentModule {}
Add import in app.module.ts
:
// Exercise 3: Include dirty checks module import here.
import { DirtyChecksComponentModule } from "./shared/dirty-checks.component";
And include it.
// Exercise 3: Include dirty checks module
DirtyChecksModule,
Add <dirty-checks>
component in app.component.ts
template:
template: `
<!-- Exercise 3: Add dirty checks here -->
<app-shell>
<dirty-checks></dirty-checks>
<router-outlet></router-outlet>
</app-shell>
`,
Serve your app and try to interact with the page. You will see counter always go up.
By default all angular component using ChangeDetectionStrategy.Default
.
This means component will be checked and template bindings will be re-evaluated all the time which causing performance degradation.
You should aim to use ChangeDetectionStrategy.OnPush
in all your components.
Let's do one simple but significant change and make our app.component.ts
use OnPush
change detection:
// Exercise 3: Set app component CD to OnPush
@Component({
selector: 'app-root',
template: `
<app-shell>
<router-outlet></router-outlet>
</app-shell>
`,
changeDetection: ChangeDetectionStrategy.OnPush,
})
- You can generate components with
OnPush
by default if you add next content toangular.json
schematics:
show solution
{
"schematics": {
"@schematics/angular": {
"component": {
"changeDetection": "OnPush"
}
}
}
}
- Use
@angular-eslint
plugins to forceOnPush
change detection.
trackBy
is a crucial performance improvement when you iterate over non-primitive sets of data.
We will optimize genres rendering in app-shell component.
In app-shell.component.ts
import list model:
// Exercise 3: Add TMDBMovieGenreModel import here
import { TMDBMovieGenreModel } from "../data-access/api/model/movie-genre.model";
Create a trackByGenre
function in the same file:
// Exercise 3: Create trackBy function here
trackByGenre(_: number, genre: TMDBMovieGenreModel) {
return genre.name;
}
Go to app-shell.component.html
and add our function to template:
<!-- Exercise 3: Use trackBy here -->
<a
[attr.data-uf]="'menu-gen-'+genre.id"
*ngFor="let genre of genres$ | async; trackBy: trackByGenre;"
class="navigation--link"
[routerLink]="['/list', 'genre', genre.id]"
routerLinkActive="active"
>
<div class="navigation--menu-item">
<svg-icon class="navigation--menu-item-icon" name="genre"></svg-icon>
{{ genre.name }}
</div>
</a>
- You can force
ngFor
+trackBy
with@angular-eslint
linting
One of the most common operators for observables that trigger HTTP requests is switchMap
.
However switchMap
can cause an over-fetching. For requests that will not change results quickly exhaustMap
operator is a better option.
This operator will ignore all higher observable emissions until it's observable finished.
This will help us improve TTI and TBT metrics.
Go to genre.state.ts
, add import of exhaustMap
and remove switchMap
:
// Exercise 3: Replace switchMap with exhaustMap
import { exhaustMap } from "rxjs";
Go to genre.state.ts
and switch to exhaustMap
:
this.actions.refreshGenres$.pipe(
// Exercise 3: Use exhaustMap here
exhaustMap(this.genreResource.getGenres)
);