DEV Community

Cover image for The comprehensive guide to Angular Performance Tuning
Harshal Suthar
Harshal Suthar

Posted on • Originally published at ifourtechnolab.com

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.

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 {}

Enter fullscreen mode Exit fullscreen mode

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.

    <xml><div class="mx-auto col-10"><h5>{{'todo-list' | translate}}</h5>
<hr><app-cards-list></app-cards-list></div>
<hr><app-add-todo></app-add-todo>
    </xml>

Enter fullscreen mode Exit fullscreen mode

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"
    }
  }

Enter fullscreen mode Exit fullscreen mode

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()
);
}

Enter fullscreen mode Exit fullscreen mode

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()
    );
  }

Enter fullscreen mode Exit fullscreen mode

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()
        );
      }

Enter fullscreen mode Exit fullscreen mode

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;
  }

Enter fullscreen mode Exit fullscreen mode

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<todoitem>();

    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);
    }
  }
</todoitem>

Enter fullscreen mode Exit fullscreen mode

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);

Enter fullscreen mode Exit fullscreen mode

Import routes:

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

Enter fullscreen mode Exit fullscreen mode

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);

Enter fullscreen mode Exit fullscreen mode

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
 })

Enter fullscreen mode Exit fullscreen mode

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.

Top comments (4)

Collapse
 
pbouillon profile image
Pierre Bouillon

Hey there, thanks for the article, it covers a lot of ground!

I do have some remarks about it:

As we previously discussed, async pipe is an example of a pure pipe.

It is not, as shown in the implementation

There is a pretty good article about this written by @eneajaho:

Also, on preloading strategies, preloading everything might not increase performances if you are loading things the user won't need, I dug into the topic in my latest article if you are interested!

Collapse
 
ifourtechnolab profile image
Harshal Suthar

Thanks.

Collapse
 
xaberue profile image
Xavier Abelaira Rueda

Really practical, thanks for sharing!

Collapse
 
ifourtechnolab profile image
Harshal Suthar

Thanks.