How to improve the performance of your Angular app

10 Apr 2020 | 15 min read
Angular app performance

When talking about the greatest frontend frameworks, it’s impossible to not mention Angular. It requires a lot of effort from programmers to learn it and use it wisely, though. Unfortunately, there is a risk that developers who are not experienced in Angular can use some of its features in an inefficient way. 

One of the many things you always need to work on as a frontend developer is the app’s performance. A large portion of my past projects focused on large, enterprise applications that continue to be expanded and developed. Frontend frameworks would be extremely useful here, but it’s important to use them correctly and reasonably. 

I have prepared a quick list of the most popular performance boost strategies and tips that may help you to instantly increase the performance of your Angular application. Please keep in mind that all of the hints here apply to Angular in version 8. 

ChangeDetectionStrategy and ChangeDetectorRef

Change Detection (CD) is Angular’s mechanism for detecting data changes and automatically reacting to them. We can list the basic kind of standard application state changes:

  • Events
  • HTTP Request
  • Timers

These are asynchronous interactions. The question is: how would Angular know that some interactions (such as click, interval, http request) occurred and there is a need to update the application state?

The answer is ngZone, which is basically a complex system meant to track asynchronous interactions. If all operations are registered by ngZone, Angular knows when to react to some changes. But it doesn’t know what exactly has changed and launches the Change Detection mechanism, which checks all components in first-depth-order.

Each component in the Angular app has its own Change Detector, which defines how this component should act when the Change Detection was launched – for example, if there is a need to re-render a component’s DOM (which is rather an expensive operation). When Angular launches Change Detection, every single component will be checked and its view (DOM) may be re-rendered by default.

We can avoid this by using ChangeDetectionStrategy.OnPush:

@Component({
  selector: 'foobar',
  templateUrl: './foobar.component.html',
  styleUrls: ['./foobar.component.scss'],
  changeDetection: ChangeDetectionStrategy.OnPush
})

As you can see in the example code above, we have to add an additional parameter to the component’s decorator. But how does this new change detection strategy really work? 

The strategy tells Angular that a specific component only depends on its @Inputs(). Also, all component @Inputs()will act like an immutable object (e.g. when we change only the property in an object’s @Input(), without changing the reference, this component won’t be checked). It means that a lot of unnecessary checking will be omitted and it should increase our app performance.

A component with ChangeDetectionStrategy.OnPush will be checked only in the following cases:

  • @Input() reference will change
  • An event will be triggered in the component’s template or one of its children
  • Observable in the component will trigger an event
  • CD will be run manually using ChangeDetectorRef service
  • async pipe is used on the view (the async pipe marks the component to be checked for changes – when the source stream will emit a new value this component will be checked)

If none of the above happens, using ChangeDetectionStrategy.OnPush inside a specific component causes the component and all the nested components to not be checked after CD launch.

Fortunately, we can still have full control of reacting to data changes by using the ChangeDetectorRef service. We have to remember that with ChangeDetectionStrategy.OnPush inside our timeouts, requests, subscriptions callbacks, we need to fire CD manually if we really need this:

counter = 0;

constructor(private changeDetectorRef: ChangeDetectorRef) {}

ngOnInit() {
  setTimeout(() => {
    this.counter += 1000;
    this.changeDetectorRef.detectChanges();
  }, 1000);
}

As we can see above, by calling this.changeDetectorRef.detectChanges() inside our timeout function, we can force CD manually. If the counter is used inside the template in any way, its value will be refreshed.

The last tip in this section is about permanently disabling CD for specific components. If we have a static component and we are sure that its state shouldn’t be changed, we can disable CD permanently:

this.changeDetectorRef.detach()

This code should be executed inside the ngAfterViewInit() or ngAfterViewChecked()  lifecycle method, to be sure that our view was rendered correctly before we disable data refreshing. This component will no longer be checked during CD, unless we trigger detectChanges() manually.

Function calls and getters in template

Using function calls inside templates executes this function each time the Change Detector is running. The same situation happens with getters. If possible, we should try to avoid this. In most cases, we don’t need to execute any functions inside the component’s template during every CD run. Instead of that we can use pure pipes.

Pure pipes

Pure pipes are a kind of pipes with an output that depends only on its input, with no side-effects. Luckily, all pipes in Angular are pure by default.

@Pipe({
    name: 'uppercase',
    pure: true
})

But why should we avoid using pipes with pure: false? The answer is Change Detection again. Pipes that are not pure are executed in every CD run, which is not necessary in most cases and decreases our app’s performance. Here is the example of the function that we can change to pure pipe:

transform(value: string, limit = 60, ellipsis = '...') {
  if (!value || value.length <= limit) {
    return  value;
  }
  const numberOfVisibleCharacters = value.substr(0, limit).lastIndexOf(' ');
  return `${value.substr(0, numberOfVisibleCharacters)}${ellipsis}`;
}

And let’s see the view:

<p class="description">truncate(text, 30)</p>

The code above represents the pure function – no side effects, output only dependent on inputs. In this case, we can simply replace this function by pure pipe:

@Pipe({
  name: 'truncate',
  pure: true
})
export class TruncatePipe implements PipeTransform {
  transform(value: string, limit = 60, ellipsis = '...') {
    ...
  }
}

And finally, in this view, we get the code, which will be executed only when the text was changed, independently from Change Detection.

<p class="description">{{ text | truncate: 30 }}</p>

Lazy loading and preloading modules

When your application has more than one page, you should definitely consider creating modules for each logical piece of your project, especially lazy loading modules. Let’s consider the simple Angular router code:

const routes: Routes = [
  {
    path: '',
    component: HomeComponent
  },
  {
    path: 'foo',
    loadChildren: ()=> import("./foo/foo.module").then(m => m.FooModule)
  },
  {
    path: 'bar',
    loadChildren: ()=> import("./bar/bar.module").then(m => m.BarModule)
  }
]
@NgModule({
  exports: [RouterModule],
  imports: [RouterModule.forRoot(routes)]
})
class AppRoutingModule {}

In the example above we can see that the fooModule with all its assets will be loaded only when the user tries to enter a specific route (foo or bar). Angular will also generate a separate chunk for this module. Lazy loading will reduce the initial load.

We can do some further optimization. Let’s assume that we want to make our app loading modules in the background. For this case, we can use the preloadingStrategy. Angular by default has two types of preloadingStrategy:

  • NoPreloading
  • PreloadAllModules

In the code above the NoPreloading strategy is used by default. The app starts to load a specific module by user request (when the user wants to see a specific route). We can change this by adding some extra config to the Router.

@NgModule({
  exports: [RouterModule],
  imports: [RouterModule.forRoot(routes, {
       preloadingStrategy: PreloadAllModules
  }]
})
class AppRoutingModule {}

This config causes the current route to be shown as soon as possible and after that the application will try to load the other modules in the background. Smart, isn’t it? But that’s not all. If this solution doesn’t fit our needs we can simply write our own custom strategy.

Let’s assume that we want to preload only selected modules, for example, BarModule. We indicate this by adding an extra field for the data field.

const routes: Routes = [
  {
    path: '',
    component: HomeComponent
    data: { preload: false }
  },
  {
    path: 'foo',
    loadChildren: ()=> import("./foo/foo.module").then(m => m.FooModule),
    data: { preload: false }
  },
  {
    path: 'bar',
    loadChildren: ()=> import("./bar/bar.module").then(m => m.BarModule),
    data: { preload: true }
  }
]

Then we have to write our custom preloading function:

@Injectable()
export class CustomPreloadingStrategy implements PreloadingStrategy {
  preload(route: Route, load: () => Observable<any>): Observable<any> {
    return route.data && route.data.preload ? load() : of(null);
  }
}

And set it as a preloadingStrategy:

@NgModule({
  exports: [RouterModule],
  imports: [RouterModule.forRoot(routes, {
       preloadingStrategy: CustomPreloadingStrategy
  }]
})
class AppRoutingModule {}

For now only routes with param { data: { preload: true } } will be preloaded. The rest of the routes will act like NoPreloading is set.

Custom preloadingStrategy is @Injectable(), so it means that we can inject some services inside if we need to and customize our preloadingStrategy in any other way.
With a browser’s developer tools, we can investigate the performance boost by equal initial loading time with and without a preloadingStrategy. We can also look at the network tab to see that chunks for other routes are loading in the background, while the user is able to see the current page without any delays.

trackBy function

We can assume that most of the Angular apps using *ngFor to iterate over items listed inside the template. If the iterated list is also editable, trackBy is absolutely a must have.

<ul>
  <tr *ngFor="let product of products; trackBy: trackByProductId">
    <td>{{ product.title }}</td>
  </tr>
</ul>

trackByProductId(index: number, product: Product) {
  return product.id;
}

By using trackBy function Angular is able to track which elements of collections have changed (by given identifier) and re-render only these particular elements. When we omit trackBy, the whole list will be re-loaded which can be a very resource-intensive operation on DOM.

Ahead-of-time (AOT) compilation

Regarding Angular documentation:

(…) components and templates provided by Angular cannot be understood by the browser directly, Angular applications require a compilation process before they can run in a browser

Angular provides the two types of compiling:

  • Just-in-Time (JIT) – compiles an app in the browser at runtime
  • Ahead-of-Time (AOT) – compiles an app at build time

For development usage JIT compilation should cover developer needs. Nevertheless, for production build we should definitely use the AOT. We need to make sure the aot flag inside the angular.json file is set to true. The most important benefits of such a solution include faster rendering, fewer asynchronous requests, smaller framework download size and increased security.

Summary

The application’s performance is something that you need to keep in mind both during the development and the maintenance part of your project. However, searching for possible solutions on your own might be both time- and effort-consuming. Checking for these commonly made mistakes and keeping them in mind during the development process will not only help you improve your Angular app’s performance in no time, but also help you avoid future lapses.

Want to develop an app with Miquido?

Thinking about giving your business a boost with an Angular app? Get in touch with us and choose our Angular development services.

Your data is processed by Miquido sp. z o.o. sp.k. with its registered office in Kraków at Zabłocie 43A, 30 - 701 Kraków. The basis for processing your data is your consent and the legitimate interest of Miquido.
You may withdraw your consent at any time by contacting us at marketing@miquido.com. You have the right to object, the right to access your data, the right to request rectification, deletion or restriction of data processing. For detailed information on the processing of your personal data, please see Privacy Policy.

Show more