top of page

10 Tricks to Optimize Your Angular App

Updated: May 4, 2023

Angular is a powerful JavaScript framework for building web applications. However, as applications grow in complexity, performance can become a major concern. Slow loading times and laggy user interfaces can lead to a poor user experience and hurt the overall success of your app. In this article, we will explore some tricks to optimize your Angular app and improve its performance. These tips and techniques will help you reduce loading times, improve user experience, and make your Angular app run smoother than ever before.



10 Tricks to Optimize Your Angular App

1. ChangeDetectionStrategy.OnPush


ChangeDetectionStrategy.OnPush is a strategy used in Angular to improve performance by reducing the number of unnecessary change detection runs. By default, Angular uses the Default strategy, which checks for changes in the component and its children whenever an event occurs. OnPush, on the other hand, only checks for changes if the component's inputs have referentially changed.


The ChangeDetectorRef class is used to detach a component from the change detection tree, preventing it from being checked during subsequent change detection runs. This is useful for components that don't need to be updated frequently, such as those with static content.


The methods available in ChangeDetectorRef include:

  • markForCheck(): Explicitly marks the view as changed so that it can be checked again.

  • detach(): Detaches the view from the change detection tree.

  • detectChanges(): Checks the view and its children for changes.

  • checkNoChanges(): Checks the change detector and its children and throws an error if changes are detected.

  • reattach(): Re-attaches the previously detached view to the change detection tree.



2. Detaching the Change Detector

To use the ChangeDetectorRef, inject it into the component's constructor and call the detach() method to detach the component from the change detection tree. To reattach the component and update the DOM with any changes, call the reattach() method and make the necessary changes to the component's data.

Here's an example of how to use ChangeDetectorRef in a TestComponent:

@Component({     
template: `<div>{{data}}</div>` 
})
class TestComponent {     
    data = 0;      
    
    constructor(private changeDetectorRef: ChangeDetectorRef) 
    {         
        changeDetectorRef.detach();     
    }      
    clickHandler() 
    {         
        changeDetectorRef.reattach();         
        data++;     
    } 
} 

In this example, the TestComponent is initially detached from the change detection tree using the detach() method. When the clickHandler() method is called, the component is reattached using the reattach() method and the data is incremented. This will trigger a change detection run, updating the DOM with the new value of data.


3. Local Change Detection

With the ability to detach components from the change detection tree, we can now trigger change detection from our component and have it run down the component sub-tree.


For example, let's consider the TestComponent shown below:

@Component({
    ...
    template: `<div>{{data}}</div>`
})
class TestComponent {
    data = 0;
    constructor(private changeDetectorRef: ChangeDetectorRef) {
        changeDetectorRef.detach();
    }
}

We can update the data-bound data property and use the detectChanges method to run change detection only for the TestComponent and its children:

@Component({
    ...
    template: `<div>{{data}}</div>`
})
class TestComponent {
    data = 0;
    constructor(private changeDetectorRef: ChangeDetectorRef) {
        changeDetectorRef.detach();
    }
    clickHandler() {
        this.data++;
        this.changeDetectorRef.detectChanges();
    }
}

In this example, the clickHandler method will increase the data value by one and call detectChanges to run change detection on the TestComponent and its children. This will cause the updated data value to be reflected on the DOM while still being detached from the change detection tree.


Local change detection is run from the component down to its children, unlike global change detection, which runs from the root down to the children. This can be a huge performance boost if the data variable updates frequently, such as every second.


4. Run outside of Angular

Angular uses NgZone/Zone to monitor asynchronous events and trigger change detection (CD) on the component tree. However, it's possible to run code outside of the Angular zone, which means that NgZone/Zone won't detect any asynchronous events and CD won't be triggered automatically. This can be useful when running code that frequently updates the UI and can cause performance issues.


In the following example, we have a TestComponent that has two methods: processInsideZone and processOutsideZone. processInsideZone runs the code inside the Angular zone, which means that any changes to the component's data property will trigger CD and update the UI. On the other hand, processOutsideZone runs the code outside the Angular zone, which means that any changes to the data property will not trigger CD, and the UI won't be updated.

@Component({
    ...
    template: `
        <div>
            {{data}}
            {{done}}
        </div>
    `
})class TestComponent {
    data = 0
    done
    constructor(private ngZone: NgZone) {}    
    processInsideZone() {
        if (this.data >= 100) {
            this.done = "Done"
        } else {
            this.data += 1
        }
    }    
    processOutsideZone() {
        this.ngZone.runOutsideAngular(() => {
            if (this.data >= 100) {
                this.ngZone.run(() => {
                    this.data = "Done"
                })
            } else {
                this.data += 1            
            }
        })
    }
}

To update the UI, we need to re-enter the Angular zone using the run method provided by the NgZone service. In the processOutsideZone method, we use the runOutsideAngular method to run the code outside the Angular zone, and then we use run to re-enter the zone and update the UI with the "Done" message when the data property is equal to or greater than 100.


Overall, running code outside the Angular zone can be useful for improving performance, but it's important to remember to re-enter the zone when necessary to update the UI.


5. Use pure pipes

Pure pipes in Angular have no side effects, which means that their behavior is predictable, and we can cache the input to avoid recomputing CPU-intensive operations. For example, imagine we have a function in a pipe that takes a lot of time before producing a result:

function bigFunction(val) {
  // ...return something;
}

@Pipe({
  name: 'util',
})class UtilPipe implements PipeTransform {
  transform(value) {
    return bigFunction(value);
  }
}

If this function is called repeatedly, it can hang the main thread and make the UI laggy for users. To avoid this, we can use a technique called memorization.


Memoization involves caching the results of the function for a given input and returning them on subsequent calls with the same input. This means that the function is only called once for each unique input, and subsequent calls with the same input use the cached result instead of recomputing the function.


To enable memoization in an Angular pipe, we can set the pure flag in the @Pipe decorator to true. This tells Angular that the pipe is pure and has no side effects:

function bigFunction(val) {
  // ...return something;
}

@Pipe({
  name: 'util',
  pure: true,
})class UtilPipe implements PipeTransform {
  transform(value) {
    return bigFunction(value);
  }
}

With this setup, Angular caches the results of the pipe for each input and returns them on subsequent calls. This can greatly improve performance, especially for pipes that perform expensive operations or are called frequently.


6. Use trackBy option for *ngFor directive

The *ngFor directive in Angular is used for iterating over iterable and rendering them on the DOM. However, it can cause performance issues due to its internal implementation.


By default, ngFor uses a differ to detect changes in the iterable and trigger a re-render of the DOM. This differ uses strict object reference comparison (i.e., the === operator) to detect changes. This can be a problem when using immutable data structures, as updating an object will create a new object reference, which will cause ngFor to continually destroy and recreate the DOM for each iterable.


This issue can be mitigated by using the trackBy option of ngFor. The trackBy option allows the developer to specify a function that identifies the unique identity of each element in the iterable. By doing so, the differ can track changes based on the identity of the elements rather than their object reference, preventing unnecessary DOM manipulations.


For example, the following code demonstrates how to use trackBy to improve the performance of ngFor:

@Component({
  selector: 'app-item-list',
  template: `
    <ul>
      <li *ngFor="let item of items; trackBy: trackByFn">{{ item.name }}</li>
    </ul>
  `,
})
export class ItemListComponent {
  items: Item[];

  constructor(private itemService: ItemService) {
    this.items = itemService.getItems();
  }

  trackByFn(index: number, item: Item) {
    return item.id; // Use the unique ID of each item as its identity
  }
}

In the above example, the trackByFn function returns the unique ID of each item as its identity. This enables ngFor to track changes based on the item ID rather than its object reference, resulting in improved performance.


7. Optimize template expressions

In Angular, template expressions are commonly used to run functions in the template. However, when these functions take a long time to complete, it can result in a slow and laggy UI experience for the users.


For example:

@Component({
    template: `
        <div>
            {{func()}}
        </div>
    `
})
class TestComponent {
    func() {
        // a long-running operation
    }
}

In this case, the func function will be run when Change Detection is triggered on the TestComponent, and it must complete before other UI codes can be run.


To avoid this issue, it's recommended to make template expressions finish quickly. If a template expression involves a highly computational function, caching can be employed on it to improve performance.


8. Web Workers

JS is a single-threaded language, this means that JS code is run on the main thread. This main thread runs algorithms and the UI algorithm.


Setting up, compiling, bundling, and code-splitting Web Workers in Angular is made easy by the CLI tool. To generate a Web Worker, you can run the ng g web-worker command, which will create a webworker.ts file in the src/app directory of the Angular app. This file tells the CLI tools that it will be used by a Worker.


To demonstrate how to use Web Workers in Angular to optimize performance, let's say we have an app that calculates Fibonacci numbers. Calculating Fibonacci numbers is recursive, and passing large numbers will have a significant impact on performance, leading to a slow UI experience. The best solution is to move the Fibonacci function or algorithm to execute in another thread. This way, no matter how large the number is, it will not impact the DOM thread.


To do this, we first scaffold a Web Worker file using the ng g web-worker command and move the Fibonacci function into the file. The file should look like this:

// webWorker-demo/src/app/webWorker.ts
function fibonacci(num) {
    if (num == 1 || num == 2) {
        return 1;
    }
    return fibonacci(num - 1) + fibonacci(num - 2);
}

self.addEventListener('message', (evt) => {
    const num = evt.data;
    postMessage(fibonacci(num));
});

Next, we edit the app.component.ts file to add Web Worker support. We add the ngOnInit lifecycle hook to initialize the Web Worker with the Web Worker file we generated earlier. We also register to listen to messages sent from the Web Worker in the onmessage handler. Any data we receive is displayed in the DOM. The calcFib function sends the number to the Web Worker, which captures the number and processes the Fibonacci number, then sends the result back to the DOM thread. The onmessage function we set up in the app.component.ts file receives the result in data and displays it in the DOM using the {{output}} interpolation.

// webWorker-demo/arc/app/app.component.ts@Component({
    selector: 'app',
    template: `
        <div>
            <input type="number" [(ngModel)]="number" placeholder="Enter any number" />
            <button (click)="calcFib()">Calc. Fib</button>
        </div>
        <div>{{output}}</div>
    `
})
export class App implements OnInit {
    private number;
    private output;
    private webworker: Worker;

    ngOnInit() {
        if (typeof Worker !== 'undefined') {
            this.webWorker = new Worker('./webWorker');
            this.webWorker.onmessage = (data) => {
                this.output = data;
            };
        }
    }

    calcFib() {
        this.webWorker.postMessage(this.number);
    }
}

During the processing of the Fibonacci numbers, the DOM thread is left to focus on user interactions while the Web Worker does the heavy processing. This results in a much faster and smoother user experience.


9. Lazy-Loading

Lazy-loading is a highly effective optimization technique used in web browsers to defer the loading of resources like images, audio, video, and web pages until they are needed. This technique significantly reduces the amount of bundled files loaded during the initial load of the webpage, only loading resources that will be used directly on the page. All other resources are not loaded until they are required by the user.


Angular makes it very easy to implement lazy-loading for resources. In order to lazy-load routes in Angular, we define the routes as follows:

const routes: Routes = [
  {
    path: '',
    component: HomeComponent
  },
  {
    path: 'about',
    loadChildren: () => import("./about/about.module").then(m => m.AboutModule)
  },
  {
    path: 'viewdetails',
    loadChildren: () => import("./viewdetails/viewdetails.module").then(m => m.ViewDetailsModule)
  }
];

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

Here, we use dynamic import to specify the resources we want to lazy load. Angular generates separate chunks for each resource specified. On the initial load of the app, these chunks are not loaded. When the user navigates to a route that requires one of the specified resources, the appropriate chunk is loaded.


For example, if the size of the whole non-lazy-loaded bundle is 1MB, lazy-loading will splice out the "about" and "viewdetails" chunks, which may be 300kb and 500kb respectively. In this case, we can see that the bundle will be cut down to 200kb, which is more than half of the original size. This results in a faster and more optimized web page.


10. Preloading

This is an optimization strategy that loads resources (webpages, audio, video files) for faster future navigations or consumption. This speeds up both loading and rendering of the resource because the resource will already be present in the browser cache.


Angular has preloading strategy implemented in @angular/router module. This allows us to preload resources, routes/links, modules, etc in Angular apps. The Angular router provides an abstract class PreloadingStrategy that all class implements to add their preloading strategy in Angular.

class OurPreloadingStrategy implements PreloadingStrategy {
    preload(route: Route, fn: ()=> Observable <any>) {
        // ...
    }
}

We specify it as the value for the preloadingStrategy property in the router configuration.

// ...
RouterModule.forRoot([
    ...
], {
    preloadingStrategy: OurPreloadingStrategy
})
// ...

0 comments

Comments


bottom of page