Telerik blogs
Angular

Check out these tips and techniques that you can use when attempting to optimize Angular applications. Learn how to use lazy loading, server-side rendering and more.

When an application grows from a couple lines of code to several files or folders of code, then every byte or second saved matters. When an application grows to that size, the word “optimization” gets whispered a lot. This is because is application of that size would typically run like a coal-powered train, but users expect a high-speed train.

Today we’ll look at some useful techniques to adopt when attempting to optimize Angular applications. These techniques are useful for improving load-time and runtime performance.

Lazy Loading

A very useful technique and one of the most recommended for a majority of web applications, lazy loading is basically load-on-demand. In this technique, some parts of your application are bundled separately from the main bundle, which means those parts load when an action is triggered. For example, you have a component called AboutComponent. This component renders the About page, and the About page isn’t the first thing a user sees when the page is loaded. So the AboutComponent can be bundled separately and loaded only when the user attempts to navigate to the About page.

To achieve lazy loading in Angular, lazy modules are used, meaning you can define modules separately from your app’s main module file. Angular naturally builds a separate bundle for each lazy module, so we can instruct Angular to only load the module when the route is requested. This technique improves load-time performance but affects runtime performance in the sense that it might take some time to load the lazy modules depending on the size of the module — that’s why Angular has a useful strategy called PreloadingStrategy.

PreloadingStrategy is used for telling the RouterModule how to load a lazy module, and one of the strategies is PreloadAllModules. This loads all the lazy modules in the background after page load to allow quick navigation to the lazied module.

Let’s look at an example.

You have a feature module called FoodModule to be lazy loaded. The module has a component called FoodTreeComponent and a routing module FoodRoutingModule.

import { NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';
import { FoodRoutingModule } from './food-routing.module';
import { FoodTreeComponent } from './food-tree/food-tree.component';

@NgModule({
  imports: [
    CommonModule,
    FoodRoutingModule
  ],
  declarations: [FoodTreeComponent]
})
export class FoodModule { }

To lazy load the FoodModule component with the PreloadAllModules strategy, register the feature module as a route and include the loading strategy:

import { NgModule } from '@angular/core';
import { FormsModule } from '@angular/forms';
import { BrowserModule } from '@angular/platform-browser';
import { PreloadAllModules, RouterModule } from '@angular/router';

import { AppComponent } from './app.component';

@NgModule({
  declarations: [
    AppComponent
  ],
  imports: [
    BrowserModule,
    FormsModule,
    RouterModule.forRoot([
      {
        path: 'food',
        loadChildren: './food/food.module#FoodModule'
      }
    ], {preloadStrategy: PreloadAllModules} )
  ],
  providers: [],
  bootstrap: [AppComponent]
})
export class AppModule { }

Change Detection Strategy

In your application, Angular runs checks to find out if it should update the state of a component. These checks, called change detection, are run when an event is triggered (onClick, onSubmit), when an AJAX request is made, and after several other asynchronous operations. Every component created in an Angular application has a change detector associated to it when the application runs. The work of the change detector is re-rendering the component when a value changes in the component.

This is all okay when working with a small application — the amount of re-renders will matter little — but in a much bigger application, multiple re-renders will affect performance. Because of Angular’s unidirectional data flow, when an event is triggered, each component from top to bottom will be checked for updates, and when a change is found in a component, its associated change detector will run to re-render the component.

Now, this change detection strategy might work well, but it will not scale, simply because this strategy will need to be controlled to work efficiently. Angular, in all its greatness, provides a way to handle change detection in smarter way. To achieve this, you have to adopt immutable objects and use the onPush change detection strategy.

Let’s see an example:

You have a component named BankUser. This component takes an Input object user, which contains the name and email of a bank user:

@Component({
  selector: 'bank-user',
  template: `
    <h2>{{user.name}}</h2>
    <p>{{user.email}}</p>
  `
})
class BankUser {
  @Input() user;
}

Now, this component is being rendered by a parent component Bank that updates the name of the user on the click of a button:

@Component({
  selector: 'the-bank',
  template: `
    <bank-user [user]="bankUser"></bank-user>
    <button (click)="updateName()">Update Name</button>
  `
})
class Bank {
  bankUser = {
    name: 'Mike Richards',
    email: 'mike@richards.com',
  }

  updateName(){
    this.bankUser.name = 'John Peters'
  }
}

On the click of that button, Angular will run the change detection cycle to update the name property of the component. This isn’t very performant, so we need to tell Angular to update the BankUser component only if one of the following conditions is met:

  • Change detection is run manually by calling detectChanges
  • The component or its children triggered an event
  • The reference of the Input has been updated

This explicitly makes the BankUser component a pure one. Let’s update the BankUser component to enforce these conditions by adding a changeDetection property when defining the component:

@Component({
  selector: 'bank-user',
  template: `
    <h2>{{ user.name }}</h2>
    <p>{{ user.email }}</p>
  `,
  changeDetection: ChangeDetectionStrategy.OnPush
})
export class BankUser {
  @Input() user;
}

After making this update, clicking the Update Name button will have no effect on the component unless we also change the format by which we update the name of the bank user. Update the updateName method to look like the snippet below:

updateName() {
  this.bankUser = {
    ...this.bankUser,
    name: 'John Peters'
  };
}

Now, clicking the button works because one of the conditions set is met — the Input reference has been updated and is different from the previous one.

TrackBy

Rendering lists can affect the performance of an application — huge lists with attached listeners can cause scroll jank, which means your application stutters when users are scrolling through a huge list. Another issue with lists is updating them — adding or removing an item from a long list can cause serious performance issues in Angular applications if we haven’t provided a way for Angular to keep track of each item in the list.

Let’s look at it this way: There’s a list of fruits containing 1,000 fruit names being displayed in your application. If you want to add another item to that list, Angular has to recreate the whole DOM node for those items and re-render them. That is 1,001 DOM nodes created and rendered when just one item is added to the list. It gets worse if the list grows to 10,000 or more items.

To help Angular handle the list properly, we’ll provide a unique reference for each item contained in the list using the trackBy function. Let’s look at an example: A list of items rendered in a component called FruitsComponent. Let’s see what happens in the DOM when we attempt to add an extra item with and without the trackBy function.

@Component({
  selector: 'the-fruits',
  template: `
    <ul>
      <li *ngFor="let fruit of fruits">{{ fruit.name }}</li>
    </ul>
    <button (click)="addFruit()">Add fruit</button>
  `,
})
export class FruitsComponent {
  fruits = [
    { id: 1, name: 'Banana' },
    { id: 2, name: 'Apple' },
    { id: 3, name: 'Pineapple' },
    { id: 4, name: 'Mango' }
  ];
  addFruit() {
    this.fruits = [
      ...this.fruits,
      { id: 5, name: 'Peach' }
    ];
  }
}

Without providing a unique reference using trackBy, the elements rendering the fruit list are deleted, recreated and rendered on the click of the Add fruit button. We can make this more performant by including the trackBy function.

Update the rendered list to use a trackBy function and also the component to include a method that returns the id of each fruit.

@Component({
  ...
  template: `
    <ul>
      <li *ngFor="let fruit of fruits; trackBy: trackUsingId">
        {{ fruit.name }}
      </li>
    </ul>
    <button (click)="addFruit()">Add fruit</button>
  `,
})
export class FruitsComponent {
  fruits = [
    ...
  ];
  ...
  trackUsingId(index, fruit){
    return fruit.id;
  }
}

After this update, Angular knows to append the new fruit to the end of the list without recreating the rest of the list.

Server-Side Rendering

Now we know lazy loading your application will save a ton of time on page load due to reduced bundle size and on-demand loading. On top of that, server-side rendering can improve the load time of the initial page of your application significantly.

Normally, Angular executes your application directly in the browser and updates the DOM when events are triggered. But using Angular Universal, your application will be generated as a static application in your server and served on request from the browser, reducing load times significantly. Pages of your application can also be pre-generated as HTML files.

Another benefit of server-side rendering is SEO performance — since your application will be rendered as HTML files, web crawlers can easily consume the information on the webpage.

Server-side rendering supports navigation to other routes using routerLink but is yet to support events. So this technique is useful when looking to serve certain parts on the application at record times before navigating to the full application. Visit this in-depth tutorial by the Angular team on how to get started with server-side rendering using Angular Universal.

Handle Change Detection

You may find instances when a component within your component tree re-renders several times within a short span of time due to side effects. This doesn’t help the highly performant cause we’re working towards. In situations like this, you have to jump in and get your hands dirty: you have to prevent your component from re-rendering.

Let’s say you have a component that has a property is connected to an observer and this observer’s value changes very often — maybe it’s a list of items that different users of the application are adding to. Rather than letting the component re-render each time a new item is added, we’ll wait and handle updating of the application every six seconds.

Look at the example below:

In this component, we have a list of fruits, and a new fruit is added every three seconds:

@Component({
  selector: 'app-root',
  template: `
    <ul>
      <li *ngFor="let fruit of fruits; trackBy: trackUsingId">
        {{ fruit.name }}
      </li>
    </ul>
    <button (click)="addFruit()">Add fruit</button>
  `,
  styleUrls: ['./app.component.scss']
})
export class AppComponent {
  constructor() {
    setInterval(() => {
      this.addFruit();
    }, 2000);
  }
  fruits = [
    { id: 1, name: 'Banana' },
    { id: 2, name: 'Apple' },
    { id: 3, name: 'Pineapple' },
    { id: 4, name: 'Mango' }
  ];
  addFruit() {
    this.fruits = [
      ...this.fruits,
      { id: 5, name: 'Peach' }
    ];
  }
  trackUsingId(index, fruit) {
    return fruit.id;
  }
}

Now imagine if this component was rendering other components that rendered other components. I’m sure you get the image I’m painting now — this component will mostly update 20 times a minute, and that’s a lot of re-renders in a minute. What we can do here is to detach the component from the change detector associated with it and handle change detection ourselves.

Since this component updates 20 times every minute, we’re looking to halve that. We’ll tell the component to check for updates once every six seconds using the ChangeDetectorRef.

Let’s update this component now to use this update:

@Component({
  selector: 'app-root',
  template: ...
})
export class AppComponent implements OnInit, AfterViewInit {
  constructor(private detector: ChangeDetectorRef) {
    // ...
  }
  fruits = [
    // ...
  ];

  // ...

  ngAfterViewInit() {
    this.detector.detach();
  }
  ngOnInit() {
    setInterval(() => {
      this.detector.detectChanges();
    }, 6000);
  }
}

What we’ve done now is to detach the ChangeDetector after the initial view is rendered. We detach in the AfterViewInit lifecycle rather than the OnInit lifecycle because we want the ChangeDetector to render the initial state of the fruits array before we detach it. Now in the OnInit lifecycle, we handle change detection ourselves by calling the detectChanges method every six seconds. We can now batch update the component, and this will improve run-time performance of your application radically.

Additional Options to Explore

We’ve looked at a few ways to optimize an Angular application. A few other notable techniques are:

  • Compressing images and lazy loading image assets: Compressing images is useful for reducing the size of images while maintaining quality. You can use image compression services like ShortPixel, Kraken and TinyPNG. You can also employ the technique of lazy loading offscreen images using APIs like IntersectionObserver or a library like ng-lazyload-image.
  • Enable prodMode: When building your application for production, you can use the enableProdMode to optimize your build for production.
  • Service workers: Service workers can be used to preload your application and serve them from cache, which enables offline functionality and reduces page load time. You can enable service worker functionality for your Angular application by following this guide.

Conclusion

Employing useful optimization techniques no matter how small and irrelevant the results may seem might go a long way to making your application run even more smoothly than it currently is. The CLI by Angular for bootstrapping your application has employed several optimization techniques, so be sure to get started using the CLI. Further optimization to your server will produce better results, so ensure you look out for those techniques. You can include useful techniques that work for your application too. Happy coding.

For More Info on Building Apps with Angular:

Check out our All Things Angular page that has a wide range of info and pointers to Angular information – from hot topics and up-to-date info to how to get started and creating a compelling UI.


About the Author

Christian Nwamba

Chris Nwamba is a Senior Developer Advocate at AWS focusing on AWS Amplify. He is also a teacher with years of experience building products and communities.

Related Posts

Comments

Comments are disabled in preview mode.