×

iFour Logo

The comprehensive guide to Angular Performance Tuning

Kapil Panchal - May 07, 2021

Listening is fun too.

Straighten your back and cherish with coffee - PLAY !

  • play
  • pause
  • pause
The comprehensive guide to Angular Performance Tuning

Improving change detection


Change detection can be the most performance-intensive part of Angular apps, so it's important to understand how to render the templates efficiently so that we would just re-rendering a component if it has new changes to display.

OnPush change detection


When an asynchronous event occurs in the app, such as click, XMLHttpRequest, or setTimeout, the default change detection behavior for components is to re-render. This can be a matter of concern because it will result in a lot of needless renderings of models that haven't been updated.

  • A new reference has been added to one of its input properties

  • An event originating from the component or one of its children, such as a click on a component button.

  • Explicit shift detection run

  • To use this technique, simply set the change-detection strategy in the component's decorator as follows:

  @Component({
    selector: 'app-todo-list',
    templateUrl: './todo-list.component.html',
    styleUrls: ['./todo-list.component.scss'],
    changeDetection: ChangeDetectionStrategy.OnPush
  })
  export class TodoListComponent implements OnInit {}

Design for immutability


Since we need a new reference given to a component's input to activate change detection with onPush, we must ensure that all state changes are immutable to use this process. If we're using Redux for state management, we'll notice that each time the state changes, we'll get a new instance, which will cause change detection for onPush components when given to a component's inputs. With this method, we'll need container components to get data from the store, as well as presentation components that can only communicate with other components via input and output.

The async pipe is the simplest way to provide store data to the template. This will appear to have the data outside of an observable and will ensure that the stream is cleaned up when the object is automatically destroyed.

            
{{'todo-list' | translate}}


Make onPush the default change detection strategy


While creating new components with Angular CLI, we can use schematics to render onPush the default changeDetection strategy. In Angular, simply add this to the schematic’s property. json is a type of data.

  "schematics": {
    "@schematics/angular:component": {
      "styleext": "scss",
      "changeDetection": "OnPush"
    }
  }

Using pipes instead of methods in templates


When a component is re-rendered, methods in a prototype will be named. Even with onPush change detection, this means it will be activated any time the component or any of its children is interacted with (click, type). If the methods perform intensive computations, the app will become sluggish as it scales because it must recompute every time the part is accessed.

Instead, we might use a pure pipe to ensure that we're just recalculating when the pipe's input shifts. As we previously discussed, async pipe is an example of a pure pipe. When the observable emits a value, it will recompute. If we're dealing with pure functions, we want to make sure we're just recomputing when the input changes. A pure function is one that, given the same input, always returns the same result. As a result, if the input hasn't changed, it's pointless to recompute the output.

public getDuedateTodayCount(todoItems: TODOItem[]) {
console.log('Called getDuedateTodayCount');
return todoItems.filter((todo) => this.isToday(new Date(todo.dueDate))).length;
}
 private isToday(someDate) {
const today = new Date();
return (
someDate.getDate() == today.getDate() &&
someDate.getMonth() == today.getMonth() &&
someDate.getFullYear() == today.getFullYear()
);
}
 

With method


Let's look at what's happening when a template system is used instead of a pipe.

Consider the following procedure:

  public getDuedateTodayCount(todoItems: TODOItem[]) {
    console.log('Called getDuedateTodayCount');
    return todoItems.filter((todo) => this.isToday(new Date(todo.dueDate))).length;
  }
  private isToday(someDate) {
    const today = new Date();
    return (
      someDate.getDate() == today.getDate() &&
      someDate.getMonth() == today.getMonth() &&
      someDate.getFullYear() == today.getFullYear()
    );
  }

With pipe


This can be solved by changing the method to a pipe, which is pure by default and will rerun the logic if the input changes.

We get the following results by building a new pipe and transferring the logic we used previously inside of it:

    import { Pipe, PipeTransform } from '@angular/core';
    import { TODOItem } from '@app/shared/models/todo-item';
    @Pipe({
      name: 'duedateTodayCount'
    })
    export class DuedateTodayCountPipe implements PipeTransform {
      transform(todoItems: TODOItem[], args?: any): any {
        console.log('Called getDuedateTodayCount');
        return todoItems.filter((todo) => this.isToday(new Date(todo.dueDate))).length;
      }
      private isToday(someDate) {
        const today = new Date();
        return (
          someDate.getDate() == today.getDate() &&
          someDate.getMonth() == today.getMonth() &&
          someDate.getFullYear() == today.getFullYear()
        );
      }

Cache values from pure pipes and functions


We can also boost this by using pure pipes by remembering/caching previous values so that we don't have to recompute if the pipe has already been run with the same input. Pure pipes don't keep track of previous values; instead, they check to see if the input hasn't changed the relationship so they don't have to recalculate. To do the previous value caching, we'll need to combine it with something else.

The Lodash memorize method is a simple way to accomplish this. Since the input is an array of objects, this isn't very realistic in this situation. If the pipe accepts a simple data type as input, such as a number, it may be advantageous to use this as a key to cache results and prevent re-computation.

Using trackBy in ngFor


While using ngFor to update a list, Angular can delete the entire list from the DOM and rebuild it because it has no way of verifying which object has been added or removed. The trackBy function solves this by allowing us to give Angular a function to evaluate which item in the ngFor list has been modified or removed, and then then re-render it.

CThis is how the track by feature looks:

  public trackByFn(index, item) {
    return item.id;
  }

For heavy computations: Detach change detection


In extreme cases, we can only need to manually enable change detection for a few components. That is, if a component is instantiated 100s of times on the same page and re-rendering each one is costly, we can disable automatic change detection for the component entirely and only cause changes manually where they are needed.

We could detach change detection and only run this when the to do Item is set in the todoItem set property if we choose to do this for the todo items:

  @Component({
    selector: 'app-todo-item-list-row',
    templateUrl: './todo-item-list-row.component.html',
    styleUrls: ['./todo-item-list-row.component.scss'],
    changeDetection: ChangeDetectionStrategy.OnPush
  })
  export class TodoItemListRowComponent implements OnInit {
    private _todoItem : TODOItem;
    public get todoItem() : TODOItem {
      return this._todoItem;
    }
    @Input()
    public set todoItem(v : TODOItem) {
      this._todoItem = v;
      this.cdr.detectChanges();
    }  
    @Input() public readOnlyTODO: boolean;
    @Output() public todoDelete = new EventEmitter();
    @Output() public todoEdit = new EventEmitter();
    @Output() public todoComplete = new EventEmitter();
  
    constructor(private cdr: ChangeDetectorRef) {}
    public ngOnInit() {
      this.cdr.detach();
    }
    public completeClick() {
      const newTodo = {
        ...this.todoItem,
        completed: !this.todoItem.completed
      };
      this.todoComplete.emit(newTodo);
    }
    public deleteClick() {
      this.todoDelete.emit(this.todoItem.id);
    }
    public editClick() {
      this.todoEdit.emit(this.todoItem);
    }
  }

Improving page load


The time it takes for a website to load is an important factor in today's user experience. Every millisecond a user waits will result in a sales loss due to a higher bounce rate and a poor user experience, so this is an area where we should focus our efforts. Faster websites are rewarded by search engines, so page load time has an effect on SEO.

We want to use Angular PWA caching, lazy loading, and bundling to improve page load time.

Cache static content using Angular PWA


Since the static content is already in the browser, caching it will make our Angular app load faster. This is easily accomplished with Angular PWA, which uses service workers to store and present static content, such as JavaScript, CSS bundles, images, and static served files, without requiring a server request.

Looking for Genuine Angular Development Company ? Enquire Today.

Cache HTTP calls using Angular PWA


We can easily set up caching rules for HTTP calls with Angular PWA to give our app a faster user experience without cluttering it with a lot of caching code. we can either optimize for freshness or efficiency, that is, read the cache only if the HTTP call times out, or check the cache first and then call the API only when the cache expires.

Lazy load routes


Lazy loading routes ensure that each function is packaged in its own bundle and that this bundle can be loaded only when it is needed.

To allow lazy loading, simply build a child route file in a function like this:

  const routes: Routes = [
  {
    path: '',
    component: TodoListCompletedComponent
  }
];
export const TodoListCompletedRoutes = RouterModule.forChild(routes);

Import routes:

 
  @NgModule({
    imports: [FormsModule, CommonModule, SharedModule, TodoListCompletedRoutes],
    declarations: [TodoListCompletedComponent]
  })
  export class TodoListCompletedModule {}

using loadChildren in the root route:

 
  const appRoutes: Routes = [
  {
    path: rootPath,
    component: TodoListComponent,
    pathMatch: 'full'
  },
  {
    path: completedTodoPath,
    loadChildren: './todo-list-completed/todo-list-completed.module#TodoListCompletedModule'
  }
];
export const appRouterModule = RouterModule.forRoot(appRoutes);

Optimizing bundling and preloading


We may choose to preload feature modules to speed up page load even further. This way, when we choose to make a lazily loaded feature module, navigation is instant.

This can be accomplished by setting PreloadModules as the preloadingStrategy:

 
RouterModule.forRoot(routes, {
preloadingStrategy: PreloadAllModules
 })

All feature modules will be loaded when the page loads, allowing us quicker page loading and instant navigation when we choose to load other feature modules. This can be further optimized by using a custom preloading Strategy like the one shown here to load only a subset of the routes on app startup

Server-side rendering with Angular Universal

It is recommended that server-side rendering be used for Angular apps that contain indexed pages. This ensures that the pages are entirely made by the server before being shown to the browser, resulting in a faster page load. This would necessitate the app not relying on any native DOM components, and instead injecting document from the Angular providers, for example.

Improving UX


Performance tuning is all about improving the bottleneck, which is the part of the system that has the most impact on the user experience. Often the alternative is simply to approach behavior with more optimism, resulting in less waiting for the customer.

Optimistic updates

Optimistic changes occur when a change is expressed in the user interface before being saved on the server. The user would have a snappier native-like experience as a result of this. As a result, in the event that the server fails to save the changes, we must roll back the state. Strongbrew has written a post on how to do this in a generic way, making positive changes simple to implement in our code.

How should we prioritize performance tuning?


Start with the low-hanging fruit: onPush, lazy loading, and PWA, and then figure out where our system's output bottlenecks are. Any enhancement that does not address the bottleneck is a mirage, as it will not enhance the app's user experience. Detaching the change detection is a tuning technique that can be used only if we have a particular issue with a component's change detection affecting output.

Conclusion


In this blog we have learned how to tune the output of our Angular app in this article. Change detection, page load, and UX enhancements were some of the performance tuning categories we looked at. Any change in a system should start with identifying bottlenecks and attempting to solve them using one of the methods described in this article.

The comprehensive guide to Angular Performance Tuning It's not uncommon to see Angular apps slow down over time. Angular is a performant platform, but if we don't know how to create performant Angular apps, our apps will become slower as they evolve. As a result, any serious Angular developer must be aware of what makes an Angular app slow in order to prevent it from being slow in the first place.   Table of Content 1. Improving change detection 2. OnPush change detection 3. Design for immutability 4. Make onPush the default change detection strategy 5. Using pipes instead of methods in templates 6. With method 7. With pipe 8. Cache values from pure pipes and functions 9. Using trackBy in ngFor 10. For heavy computations: Detach change detection 11. Improving page load 12. Cache static content using Angular PWA 13. Cache HTTP calls using Angular PWA 14. Lazy load routes 15. Optimizing bundling and preloading 16. Server-side rendering with Angular Universal 17. Improving UX 18. Optimistic updates 19. How should we prioritize performance tuning? 20. Conclusion Improving change detection Change detection can be the most performance-intensive part of Angular apps, so it's important to understand how to render the templates efficiently so that we would just re-rendering a component if it has new changes to display. OnPush change detection When an asynchronous event occurs in the app, such as click, XMLHttpRequest, or setTimeout, the default change detection behavior for components is to re-render. This can be a matter of concern because it will result in a lot of needless renderings of models that haven't been updated. A new reference has been added to one of its input properties An event originating from the component or one of its children, such as a click on a component button. Explicit shift detection run To use this technique, simply set the change-detection strategy in the component's decorator as follows: @Component({ selector: 'app-todo-list', templateUrl: './todo-list.component.html', styleUrls: ['./todo-list.component.scss'], changeDetection: ChangeDetectionStrategy.OnPush }) export class TodoListComponent implements OnInit {} Design for immutability Since we need a new reference given to a component's input to activate change detection with onPush, we must ensure that all state changes are immutable to use this process. If we're using Redux for state management, we'll notice that each time the state changes, we'll get a new instance, which will cause change detection for onPush components when given to a component's inputs. With this method, we'll need container components to get data from the store, as well as presentation components that can only communicate with other components via input and output. The async pipe is the simplest way to provide store data to the template. This will appear to have the data outside of an observable and will ensure that the stream is cleaned up when the object is automatically destroyed. {{'todo-list' | translate}} Make onPush the default change detection strategy While creating new components with Angular CLI, we can use schematics to render onPush the default changeDetection strategy. In Angular, simply add this to the schematic’s property. json is a type of data. "schematics": { "@schematics/angular:component": { "styleext": "scss", "changeDetection": "OnPush" } } Using pipes instead of methods in templates When a component is re-rendered, methods in a prototype will be named. Even with onPush change detection, this means it will be activated any time the component or any of its children is interacted with (click, type). If the methods perform intensive computations, the app will become sluggish as it scales because it must recompute every time the part is accessed. Read More: Accessibility With Angular Instead, we might use a pure pipe to ensure that we're just recalculating when the pipe's input shifts. As we previously discussed, async pipe is an example of a pure pipe. When the observable emits a value, it will recompute. If we're dealing with pure functions, we want to make sure we're just recomputing when the input changes. A pure function is one that, given the same input, always returns the same result. As a result, if the input hasn't changed, it's pointless to recompute the output. public getDuedateTodayCount(todoItems: TODOItem[]) { console.log('Called getDuedateTodayCount'); return todoItems.filter((todo) => this.isToday(new Date(todo.dueDate))).length; } private isToday(someDate) { const today = new Date(); return ( someDate.getDate() == today.getDate() && someDate.getMonth() == today.getMonth() && someDate.getFullYear() == today.getFullYear() ); } With method Let's look at what's happening when a template system is used instead of a pipe. Consider the following procedure: public getDuedateTodayCount(todoItems: TODOItem[]) { console.log('Called getDuedateTodayCount'); return todoItems.filter((todo) => this.isToday(new Date(todo.dueDate))).length; } private isToday(someDate) { const today = new Date(); return ( someDate.getDate() == today.getDate() && someDate.getMonth() == today.getMonth() && someDate.getFullYear() == today.getFullYear() ); } With pipe This can be solved by changing the method to a pipe, which is pure by default and will rerun the logic if the input changes. We get the following results by building a new pipe and transferring the logic we used previously inside of it: import { Pipe, PipeTransform } from '@angular/core'; import { TODOItem } from '@app/shared/models/todo-item'; @Pipe({ name: 'duedateTodayCount' }) export class DuedateTodayCountPipe implements PipeTransform { transform(todoItems: TODOItem[], args?: any): any { console.log('Called getDuedateTodayCount'); return todoItems.filter((todo) => this.isToday(new Date(todo.dueDate))).length; } private isToday(someDate) { const today = new Date(); return ( someDate.getDate() == today.getDate() && someDate.getMonth() == today.getMonth() && someDate.getFullYear() == today.getFullYear() ); } Cache values from pure pipes and functions We can also boost this by using pure pipes by remembering/caching previous values so that we don't have to recompute if the pipe has already been run with the same input. Pure pipes don't keep track of previous values; instead, they check to see if the input hasn't changed the relationship so they don't have to recalculate. To do the previous value caching, we'll need to combine it with something else. The Lodash memorize method is a simple way to accomplish this. Since the input is an array of objects, this isn't very realistic in this situation. If the pipe accepts a simple data type as input, such as a number, it may be advantageous to use this as a key to cache results and prevent re-computation. Using trackBy in ngFor While using ngFor to update a list, Angular can delete the entire list from the DOM and rebuild it because it has no way of verifying which object has been added or removed. The trackBy function solves this by allowing us to give Angular a function to evaluate which item in the ngFor list has been modified or removed, and then then re-render it. CThis is how the track by feature looks: public trackByFn(index, item) { return item.id; } For heavy computations: Detach change detection In extreme cases, we can only need to manually enable change detection for a few components. That is, if a component is instantiated 100s of times on the same page and re-rendering each one is costly, we can disable automatic change detection for the component entirely and only cause changes manually where they are needed. We could detach change detection and only run this when the to do Item is set in the todoItem set property if we choose to do this for the todo items: @Component({ selector: 'app-todo-item-list-row', templateUrl: './todo-item-list-row.component.html', styleUrls: ['./todo-item-list-row.component.scss'], changeDetection: ChangeDetectionStrategy.OnPush }) export class TodoItemListRowComponent implements OnInit { private _todoItem : TODOItem; public get todoItem() : TODOItem { return this._todoItem; } @Input() public set todoItem(v : TODOItem) { this._todoItem = v; this.cdr.detectChanges(); } @Input() public readOnlyTODO: boolean; @Output() public todoDelete = new EventEmitter(); @Output() public todoEdit = new EventEmitter(); @Output() public todoComplete = new EventEmitter(); constructor(private cdr: ChangeDetectorRef) {} public ngOnInit() { this.cdr.detach(); } public completeClick() { const newTodo = { ...this.todoItem, completed: !this.todoItem.completed }; this.todoComplete.emit(newTodo); } public deleteClick() { this.todoDelete.emit(this.todoItem.id); } public editClick() { this.todoEdit.emit(this.todoItem); } } Improving page load The time it takes for a website to load is an important factor in today's user experience. Every millisecond a user waits will result in a sales loss due to a higher bounce rate and a poor user experience, so this is an area where we should focus our efforts. Faster websites are rewarded by search engines, so page load time has an effect on SEO. We want to use Angular PWA caching, lazy loading, and bundling to improve page load time. Cache static content using Angular PWA Since the static content is already in the browser, caching it will make our Angular app load faster. This is easily accomplished with Angular PWA, which uses service workers to store and present static content, such as JavaScript, CSS bundles, images, and static served files, without requiring a server request. Looking for Genuine Angular Development Company ? Enquire Today. See here Cache HTTP calls using Angular PWA We can easily set up caching rules for HTTP calls with Angular PWA to give our app a faster user experience without cluttering it with a lot of caching code. we can either optimize for freshness or efficiency, that is, read the cache only if the HTTP call times out, or check the cache first and then call the API only when the cache expires. Lazy load routes Lazy loading routes ensure that each function is packaged in its own bundle and that this bundle can be loaded only when it is needed. To allow lazy loading, simply build a child route file in a function like this: const routes: Routes = [ { path: '', component: TodoListCompletedComponent } ]; export const TodoListCompletedRoutes = RouterModule.forChild(routes); Import routes:   @NgModule({ imports: [FormsModule, CommonModule, SharedModule, TodoListCompletedRoutes], declarations: [TodoListCompletedComponent] }) export class TodoListCompletedModule {} using loadChildren in the root route:   const appRoutes: Routes = [ { path: rootPath, component: TodoListComponent, pathMatch: 'full' }, { path: completedTodoPath, loadChildren: './todo-list-completed/todo-list-completed.module#TodoListCompletedModule' } ]; export const appRouterModule = RouterModule.forRoot(appRoutes); Optimizing bundling and preloading We may choose to preload feature modules to speed up page load even further. This way, when we choose to make a lazily loaded feature module, navigation is instant. This can be accomplished by setting PreloadModules as the preloadingStrategy:   RouterModule.forRoot(routes, { preloadingStrategy: PreloadAllModules }) All feature modules will be loaded when the page loads, allowing us quicker page loading and instant navigation when we choose to load other feature modules. This can be further optimized by using a custom preloading Strategy like the one shown here to load only a subset of the routes on app startup Server-side rendering with Angular Universal It is recommended that server-side rendering be used for Angular apps that contain indexed pages. This ensures that the pages are entirely made by the server before being shown to the browser, resulting in a faster page load. This would necessitate the app not relying on any native DOM components, and instead injecting document from the Angular providers, for example. Improving UX Performance tuning is all about improving the bottleneck, which is the part of the system that has the most impact on the user experience. Often the alternative is simply to approach behavior with more optimism, resulting in less waiting for the customer. Optimistic updates Optimistic changes occur when a change is expressed in the user interface before being saved on the server. The user would have a snappier native-like experience as a result of this. As a result, in the event that the server fails to save the changes, we must roll back the state. Strongbrew has written a post on how to do this in a generic way, making positive changes simple to implement in our code. How should we prioritize performance tuning? Start with the low-hanging fruit: onPush, lazy loading, and PWA, and then figure out where our system's output bottlenecks are. Any enhancement that does not address the bottleneck is a mirage, as it will not enhance the app's user experience. Detaching the change detection is a tuning technique that can be used only if we have a particular issue with a component's change detection affecting output. Conclusion In this blog we have learned how to tune the output of our Angular app in this article. Change detection, page load, and UX enhancements were some of the performance tuning categories we looked at. Any change in a system should start with identifying bottlenecks and attempting to solve them using one of the methods described in this article.

Build Your Agile Team

Enter your e-mail address Please enter valid e-mail

Categories

Ensure your sustainable growth with our team

Talk to our experts
Sustainable
Sustainable
 

Blog Our insights

Power Apps vs Power Automate: When to Use What?
Power Apps vs Power Automate: When to Use What?

I often see people asking questions like “Is Power App the same as Power Automate?”. “Are they interchangeable or have their own purpose?”. We first need to clear up this confusion...

Azure DevOps Pipeline Deployment for Competitive Business: The Winning Formula
Azure DevOps Pipeline Deployment for Competitive Business: The Winning Formula

We always hear about how important it is to be competitive and stand out in the market. But as an entrepreneur, how would you truly set your business apart? Is there any way to do...

React 18 Vs React 19: Key Differences To Know For 2024
React 18 Vs React 19: Key Differences To Know For 2024

Ever wondered how a simple technology can spark a revolution in the IT business? Just look at React.js - a leading Front-end JS library released in 2013, has made it possible. Praised for its seamless features, React.js has altered the way of bespoke app development with its latest versions released periodically. React.js is known for building interactive user interfaces and has been evolving rapidly to meet the demands of modern web development. Thus, businesses lean to hire dedicated React.js developers for their projects. React.js 19 is the latest version released and people are loving its amazing features impelling them for its adoption.