Browse Articles

7 Tips to Optimize Your Angular App/Application

 Print   20 min read  
22 Jul 2022
Advanced
10.1K Views

Angular is by default provide superfast Performance, but when it comes to designing complex and realistic application, you will start seeing cracks inside your application performance. The reason behind performance degrade can be

  • The team haven’t followed best practices

  • The solution is following best practices, but there is a need for optimization.

    The 1 st case can be easily resolved by enforcing best practices and style-guide in a current codebase. For the 2nd case, you can optimize the current codebase by applying some ways to tackle performance threats. So, here we’re going to look at 7 performance tips

  • Controlling Change Detection

  • Lazy Loading

  • AOT compilation mode + compression

  • Use Pure Pipes

  • Use trackBy function on ngFor

  • Don’t try to use Angular for everything

  • Service Worker and Server-side rendering

1. Controlling Change Detection

Angular uses unidirectional data flow when it comes to updating bindings on a page. Before getting into this, we should be aware of “what is change detection?” Well, Change Detection is a process by which Angular updates respective model values on a view (HTML). Let’s dig a little deeper into change detection. If you’re familiar with AngularJS (1.x), this process was known as a digest cycle, it's not exactly similar to Change Detection, but the philosophy remains the same.

How to change detection works

The angular application is consisting of a tree of components, each component has it's own `ChangeDetectorRef` provider, which is responsible for handling binding updates local to that component. When the application is bootstrapped, it initially creates `ApplicationContext`(root) and fires the` tick` method on the root component, which starts running change detection on each component from the top of the bottom of the tree.

“`tick` method is responsible for running change detection (detect changes method), on each component.” Basically, there are a couple of change detection strategies,

  1. Default (default strategy for a component, if not set)

  2. OnPush

“Default” Change Detection Strategy

Generally, when change detection fires at any component level, it starts triggers from the root component. It evaluates all the bindings of a component, irrespective of passed `@Input` binding has been changed or not. In a real application, this kind of change detection behavior might lead to performance impact. Assume a page where there is 100th level for a component tree and some intermediate level component triggered change detection, which eventually triggers all components binding evaluation from top to bottom. That means even though we know that we shouldn’t be running change detection for all other components, we are unintentionally running change detection for every component.

(change detection – default strategy)

In the above diagram, you can see that on the B component it ran change detection, after that it will run a `tick` method that eventually runs change detection from the root component to the leaf components.

A component changes the value of B components `Input` binding. Our all components on default strategy, it runs change detection overall components like shown in the above diagram. So what you can see is, even though we were highly interested to run the change detection on B’s component tree, it's happening all over. Fortunately, this nature can be easily tackled by toggling the change detection strategy OnPush.

“OnPush” Change Detection Strategy

When you set the change detection strategy to `OnPush`, it makes our component smarter. It calls the current component change detection method only when the incoming `@Input` bindings value would change. In other words, we can say, it runs change detection for descendant components only if Input binding is changed. This helps to save much unnecessary change detection iteration, in the end, this improves the performance of your application.

(change detection – onpush strategy)

In the above diagram, you can see components B and C are set to the OnPush strategy, so once change detection happens on component B, it triggers from the root of the tree to the bottom. And root component changes `Input` property of component `B`

As shown in the above diagram, it will only run change detection for component B and its descendant, since its Input property got to change. And change detection on component C doesn’t fire. Great! This is what we wanted to do. “One of the major performance wins would be, try to run change detection cycles as little as you can. Even removing ZoneJS from the application and running change detection manually could improve performance significantly”

TL;DR

Now the question is

  • these two flavors are enough to handle uses cases?

  • Are there any different change detection flavors that exist?

Okay! Consider you have a big application that has a real-time widget that is constantly pooling data (each 300ms) from a server like chat widget, live score, stocks value, etc. In such cases, if you keep components in the Default / OnPush strategy, you will be having multiple change detection cycles happening on each data pool operation. And if you have this kind of widget in an extensively large application, your other components will run their CD even though they are not necessary. To avoid such cases, you can easily use this technique to detach the component when you don’t want to pay a cost in terms of performance. And then you’ve to take care of running change detection manually by calling the `markForCheck` method on the current component. Oh! What are `detach` and `markForCheck`? Don’t worry we will look at them one by one.

How to detach component from change detector tree

If you look at ChangeDetectorRef API, there are several methods available, that can also be utilized to boost performance, methods namely are detached, markForCheck, retach, etc. For setting this you have to inject `ChangeDetectorRef` dependency on the component constructor and then call the `detach` method on the `changeDetectorRef` object to take out the component from the change detector tree. This line will pluck out a current component from the change detection tree. What that means is, running change detection inside the application wouldn’t run change detection to this component and its descendant.

 export class AppComponent { 
 constructor(cd: ChangeDetectorRef) { 
 cd.detach() 
 } 
}
 

And whenever you want to fire change detection on a detached component, you have to call the `markForCheck` method on that component which will eventually mark that component path from root(top) component to detached component, and update bindings only for the next change detection run.

mySpecificMethod() { 
// awesome logic here 
// once done with the call, markForCheck method which will run CD for next run only 
this.cd.markForCheck();
}

(calling a markForCheck method to run change detection on detaching tree)

(detach component from change detector tree)

(detach component from change detector tree)

In the above diagram, you can see that the C component has been set detached from the component tree, basically, that does pluck out that component (B) tree with its descendant separately. No change detection will be running implicitly on this C component and its descendants.

(calling a markForCheck method on component C)

So when it comes to running change detection on the plucked tree, consider explicitly calling the `markForCheck` method, which will traverse the tree up and mark the path till root component. Over here it has been marked as “component C to component B”. Thereafter it runs the change detection cycle over the recorded path.

TL;DR + TL;DR

Although there is one more method that exists on the `ChangeDetectorRef` provider, which is `reattach`, it is responsible for bringing back your plucked component tree to its original state. For the same, you can call the `reattach` method on the `changeDetectorRef` instance. You can refer to code snippet for using the same

 mySpecificMethod() { 
// awesome logic here 
// rettach component back to tree 
this.cd.reattach(); 
}
 

In the above diagram, you can see that component C calls reattach method, and then it brings back the plucked component tree in the change detector tree. What that means is, the upcoming change detection will be running on component C and its descendants.

2. Lazy Loading

While crafting an application, we divide major functionalities into a set of chunks, and those chunks are known as modules or sub-modules. Each module provides a unique feature. These features are pluggable and good to maintain. But when we load a Large Application consisting of 10+ modules at a single go. Eventually, code volume gets increase, effectively the final app bundle size grows tremendously. Loading large JS files on the browser can affect the initial boot time, which is not good. Ideally, an application should load faster. An ideal time is 3 seconds.

const routes: Routes = [ 
 {path: 'dashboard', component: 'DashboardModule'}, 
 {path: 'admin', component: 'AdminModule'}, 
 {path: 'purchase', component: 'PurchaseModule'}, 
 {path: '**', redirectTo: 'dashboard'} 
] 
 
@NgModule({ 
imports: [RouterModule.forRoot(routes)], 
exports: [RouterModule] }) 
export class AppRoutingModule { 
}

Don’t you see something wrong by looking at the above code? We are loading all modules and components on the initial page load, even though there is only a single module functional on the screen. That means on the initial page load Dashboard, Admin, and Purchase module is going to be loaded. Think! Can’t we make a system better? We can think of initially loading those modules which are functional on that particular screen. Yes! You’re absolutely right. This feature is already applicable in Router API. It allows you to load modules on demand and lazily based on the route. This will show you tremendous performance gain in application boot-up speed, as a web pack split bundles for each application route. Let’s refactor the previous code

 const routes: Routes = [ 
 {path: 'dashboard', loadChildren: './admin#DashboardModule'}, 
 {path: 'admin', loadChildren: './admin#AdminModule'}, 
 {path: 'purchase', loadChildren: './admin#PurchaseModule'}, 
 {path: '**', redirectTo: 'dashboard'} 
] 
 
@NgModule({ 
imports: [RouterModule.forRoot(routes)], 
exports: [RouterModule] }) 
export class AppRoutingModule { 
}

Did you see something different in the above code? Yes, the `loadChildren` option on a route. It's an option where you can mention particular angular `NgModule` path followed by `#ModuleName`. This helps to load the relevant bundle of an application based on the matched route. By having this thing in place, we ensure that we will be paying the cost for what we’re seeing on the screen.

3. AOT compilation mode + compression

Angular can be run in two different phases

  1. Just in Time (JIT) Compilation Mode

  2. Ahead of Time (AOT) Compilation Mode

When application build is created in JIT Compilation mode, it only transpile TS files to JS. These transpiled files are sent to the browser. Thereafter browser process these files further with the help of Angular Interpreter (i.e. @angular/compiler) API. That eventually converts angular files `*.*.ngfactory.js` file. Also, it collates the component and HTML code inside the JavaScript function and later those files are used by a browser to run the Angular application. The thorough conversion process is taken care of by the `@angular/compiler` module on the browser, that’s why you have to ship the `@angular/compiler` module to the browser in JIT mode, which size is somewhere around 1MB approx. In total when you run an application in JIT mode you will see below factors that affect the initial boot uptime

(Total time is taken by JIT mode)

In the above diagram, you can see that the application does a lot of things behind the scenes before it starts. This reasonably degrades the performance in terms of initial page load. In JIT mode we require to ship the @angular/compiler package which is huge (approx. 1MB). As the application gets bigger you start seeing more impact on application boot uptime. You must be thinking about the alternative approach to this. Yes! there is. There is another way that exists called AOT (Ahead of Time compilation)

AOT – Ahead of Time Compilation

Let’s see what AOT means, AOT stands for Ahead of Time compilation mode. In this phase angular compiler generates the ngfactory files at the time of creating a build package, rather than doing this on browse, it does it ahead of time. If we compare this with JIT mode, we can save several seconds and will eventually not shipping `@angular/compiler` to the browser. Ultimately that will improve initial load time. (You can’t use AOT mode if you’re loading your components dynamically, you have to use JITCompiler in that case). If you want to see the AOT version of files locally, do run the `ngc` command in the command line, which will generate *.ngfactory.js files.

4. Use Pure Pipes

Angular has something called Pipe. It is used to display the model values on UI in a slightly different format. Eg. `date | shortdate` here short date filter will convert date object to shorter date format like `dd/MM/yyyy`. You can also use Pipe to slice and dice data for display purposes Pipes are can be categorized into two parts

  1. Impure Pipe

  2. Pure Pipe

In short, the difference between pure and impure pipe is Impure pipe can produce different output for similar input over time, whereas pure pipe means a function that can produce similar output for the same input. Angular has provided certain inbuilt pipes, all of them are pure in nature. Basically, when it comes to binding evaluation, angular each time evaluate an expression and thereafter applies the pipe over it (if exists). It caches the value for that specific binding. Similar to that of memorization. Later if the same binding value tries to evaluate, angular fetch that value from the binding level cache. This out-of-the-box memoization is done by angular for pure pipes. It’s recommended to use pure pipes for performance reasons. Pure pipe only helps in the same expression evaluation consecutive amount of time. But in enterprise applications we don’t have a simple use case, we need more than this. So, you can implement the memorization of your own. Two get more performance benefit, which means once a value is visited, those values are cached, and next time if the same value arrives then the cached value will be returned.

import { Pipe, PipeTransform } from '@angular/core'; 
import { DecimalPipe } from '@angular/common'; 
import { UtilityService } from '../services/utility.service'; 

let cache = {}; // caching variable 
@Pipe({ name: 'calculateAmout' }) 
export class CalculateAmoutPipe implements PipeTransform { 
constructor(private utilityService: UtilityService, private decimalPipe: DecimalPipe){
} 
/* Calculate and save the value into the cache, and return value from cache.
It could save some iteration of calculation by memoization */ 
private calculateMemoize = (val) => { 
if(!cache[val]) {
cache[val] = this.numberFormat(val); 
return cache[val]; 
} 
private numberFormat = (value) =>{
 var result = this.utilityService.calculateBalance(value); 
 return this.decimalPipe.transform(result, '.0-0') 
}; 

transform(value: any, args?: any): any { 
 return this.calculateMemoize(value); 
 }
}

5. Use Trackby on ngFor

I assume that you must be familiar with the `*ngFor` directive, it helps to repeat the same template for each item of the collection. Consider a scenario where you’re retrieving a list of employees and displaying all employee’s data on the page. From that page, if a user adds a new employee from modal, we re-fetch the list from the server and reassign the value to the list variable. That causes a re-rendering of that `ngFor` directive. Most of the time we only accept to list get appended with a newly added element when a new collection is re-fetched. It seems like `ngFor `unnecessarily re-rendering the DOM. This could be an expensive process when rendering a large collection on the page.

NOTE

Add and removal of DOM are some of the most expensive operations on the browser, actually, those are not that expensive, but eventually, it causes the layout to redraw and repaint, which is more expensive.

Ideally, for such cases, we should not have to re-render the whole list, we should just append the newly added element in the DOM tree. That’s it. So you must be thinking how can we achieve this? Simple, you can use the `ngForTrackBy` option which will help us to make `*ngFor` directive rendering smarter. `ngForTrackBy` accepts a function that returns the identity to compare with. So, what happens behind the scenes is, while rendering template for each iteration, it stores value to default value against each template, so next time before rendering the template it checks for the ngForTrackBy function value, it is changed then only it re-renders the template, otherwise it keeps old in-memory DOM in the same place. This way performance boost is gained.

6. Don’t try to use Angular for everything

Assume you got a requirement, you are supposed to craft a page, where you should have a stacked list of users when you move the mouse over each user-item it should be highlighted with green color. This can be achieved in several different ways. But generally, people do look in for the Angular way of doing it. One can also do use `ngClass` with some `customFlag` and that `customFlag` value is toggled based on `mouseover` and `mouseout` events. Hola! Hence, we have implemented a solution in an angular way. Over here we can add a `highlight` flag over each element and toggle it based on mouseenter and mouseout event.

Component

 
import { Component } from '@angular/core'; 
@Component({ . . . }) 
export class AppComponent { 
items = [
 {id:1, name: 'Test'}, 
 {id:2, name: 'Test 2'} 
 ]
}
 

template

 <ul>
 <li (mouseenter)="i.highlight = true" (mouseleave)="i.highlight = false" [ngclass]="{highlight: i.highlight}" 
 *ngfor="let i of items">{{i.name}} </li>
 </ul>

Stackblitz here

Do you really think this is the perfect way of doing it? Think a bit! Okay, this is not a good way of doing such a thing. You must be wondering, why? If you look at it closely, you have used two events for toggling the `highlight` variable which means you’re going to run two change detection cycles for each `mouseover` and `mouseout` event, ultimately change detection is gonna fired up multiple times based on how the user is interacting on this page.FYI Angular performs change-detection when:

  1. Event callback

  2. Network messages (XHR’s)

  3. Timer, Interval.

As already discussed change detection should run as little as possible for performance reasons. So here on each mouseenter and mouseleave change detection fires most frequently. This could hamper performance in a huge application. The best way to solve this problem would be using the native way, you can easily `:hover` pseudo-class to attach highlight behavior. No need to use any kind of event or flag to maintain the hover status of the element.

Template

 <ul>
 <li *ngfor="let i of items">{{i.name}}</li>
 </ul>
 

CSS

li:hover { background: yellow }

“Don’t try to solve every problem Angular way, think simple, try out a native way as well”

7. Service Worker and Server-side rendering

The service worker is a new browser feature by which we can run event-driven scripts that can run independently. Ultimately it helps to improve performance by shifting some of the main thread tasks. Also, you can cache your API response and HTML pages, to boost up page load time. This feature can be leveraged to implement an offline mode of an application. Service worker provides access to `navigator` API which in turn opens doors to get access to various native features like push notification, geolocation, etc.

There are many big giants that always want to keep their users happy, so they completely focus on performance metrics like TTIB, TFFR, etc. on the landing page to provide non-hesitating behavior to end-user. For solving these problems, you can take benefit of navigator API and implement PWA to suppress some of these issues. You can use ` ng add @angular/pwa` to get ready PWA setup in an angular app. Another aspect you can look into is server-side rendering, this is a classic technique how we render our page. This technique can be used in Angular to improve time to first meaningful paint(FMP) metrics using Angular universal API.

What happens in server-side rendering is, when a request is sent to the server, the initial page gets rendered on the server-side and the server sends fully render HTML to the client, and the client renders it directly on the page. Thereafter as soon as client-side scripting kicks in and re-hydrate the page to make CSR code work. Server-side rendering significantly improves the initial page rendering. SSR is the most important aspect for SEO purposes and social link previewing. When a crawler comes on the page to crawl, javascript is disabled when a page is loaded for a crawler. Server-side rendering can help to solve this problem. Though it has a few gotchas that you have to take care of, thus SSR is not straightforward to implement. Check Angular Universal Docs.

Note

You should consider using server-side rendering only when you want fast First Meaningful Paint, SEO, and social link preview functionality.

Take Away

I hope you had learned about, how to get a hold of the performance of any angular application. Having these all optimization techniques incorporated inside your application could help you to gain a huge performance win. Angular training programs help you learn to build projects using web API ASP.NET  MVC5  and node.js. If you have any further queries, please feel free to ask. If you like this article, then don’t hesitate to share it with your peer fellows.

Learn to Crack Your Technical Interview

Accept cookies & close this