Thursday, March 28, 2024

How To Optimize Angular Applications

Angular remains one of the most sought-after frameworks for JavaScript on the market today and all signs seem to indicate that Angular is not going to be going anywhere anytime soon, especially as more web and mobile apps are being developed out of this popular JS framework.

Angular is a powerful Javascript framework and provides fast performance for most apps, but when it comes to designing complex architectural applications or as applications grow in size, developers may find their application starts to become slow. If the application is not performing well it will directly impact the end-user experience, which is something every programmer wants to avoid. Modern users can sometimes lack patience and may get annoyed if an app does not work at optimal speed; a user may decide to stop using an app or switch to a similar app.

With this in mind it is important to understand the underlying cause of poor performance in Angular applications. Some common causes of degradation in performance include:

  • Best practices are not followed.
  • Despite following best practices, there is still a need for optimization.

What Causes Angular App Performance Issues?

Application performance is a crucial factor in determining the end-user’s experience. There are some signs we can use to determine whether an application is performing well or not, including:

  • Sharp reduction in the data traffic of your app.
  • Reduction in engagement rate.
  • High bounce rate.

If you notice any of the above, it is recommended that you take your Angular app performance seriously and work on exploring the reasons why things are going wrong. There may be few other factors that hinder Angular applications from performing optimally, such as:

  • Frequent slowdowns in the application.
  • Slow page response.
  • App crashes unexpectedly.
  • Using server space unnecessarily.

How to Fix Angular App Performance Issues

Fixing application performance issues can be solved using Angular optimization techniques. The first step is to analyze the application and find the cause of the poor performance. Examine the code and determine if the best practices were followed. If not, you need to make sure that the app is enforcing best practices and adhering to style guides and clean code architecture.

I have witnessed lots of clients switch to React or Vue when Angular apps start performing poorly. However, I explain to them the benefits of Angular applications and make them aware of performance optimization tips like the ones included in this section.

To make them easier to understand, I have divided the tips to boost the performance of Angular application broadly into two categories:

  • Runtime performance
  • Load-time performance

Angular Runtime Performance Tips

Here is the list of some performance optimization techniques for improving the runtime of your Angular applications.

Ahead-of-Time (AOT) Compilation

Angular uses Typescript by default. Browsers only understand Javascript, so we need to compile Typescript into Javascript (a process called Transpiling) in order for it to be executed in the browser. Angular contains two compilation modes:

  • Just-In-Time (JIT)
  • Ahead-of-Time (AOT)

JIT compilation is performed at runtime. JIT compilation increases the rendering time as the compiler is also part of the bundle and code is compiled at runtime.

Ahead-of-Time (AOT) compilation is performed at build time. In AOT, the Angular compiler generates ngFactory files while creating a build package instead of doing this in the browser. In this case, we are not shipping ‘angular/compiler’ to the browser. Instead, the Angular compiler is removed from the deployment bundle, resulting in reduced app payload and improved initial load time.

Angular AOT Compiler

Change Detection in Your Angular App

One of the important, common features of the Angular framework is known as change detection. When there is a change in user data, change detection is responsible for updating DOM to reflect the respective changes.

Default Change Detection

Angular detects changes within a tree of components. It checks for changes starting from the root element, then its children, and then grandchildren, and so on. All of these changes are applied to DOM in one batch.

You may be asking whether it is a good idea to check every component on every change. Is it necessary to check all components starting from the root component to its descendants? We answer this question in the next section.

The same thing happens in the case of Reference type (objects) also. Whenever any change occurs, Angular starts checking each property of the objects to see if there is some change and then updates the DOM accordingly. Consider a scenario where we have a large object that is used in a component as well as its child components. If some change occurs, Angular will check each property for all of the objects used in that component and its descendant components. This affects the performance of an application.

OnPush Change Detection Strategy

@Component({
      selector: ‘child-component',
      changeDetection: ChangeDetectionStrategy.OnPush,
      template: `…`
 })

The OnPush strategy makes our component smarter. It makes the change detection method run only when there is a change in the incoming ‘@Input’ binding value. In other words, if the value of ‘@Input’ binding is changed then it only runs the change detection for the current component and its descendant. With no unnecessary change detection iterations the performance of your application should improve.

Detaching Components from Change Detector Tree

There may be a situation when we don’t want to run change detector on a component, such as cases where it is carrying out heavy computations. In that case it’s a good idea to keep that component out of change detection. That means when change detection runs inside the application, that component and its descendant will be plucked out of the change detection tree. Here is how that works in code:

abstract class ChangeDetectorRef {
        abstract markForCheck(): void
        abstract detach(): void
	abstract checkNoChanges(): void        
	abstract detectChanges(): void
        abstract reattach(): void
}       

In the ChangeDetectorRef API, there are several methods available to accomplish the above – namely retach, detached, markForCheck,and so forth. You need to inject ‘ChangeDetectorRef’ in the component and call the ‘detach’ method to take out the component from the change detection tree.

Here is another example:

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

Using Pure Pipes

Angular uses pipes to transform data. Using a pipe enhances performance. Pipes are the single function that takes an input and produces output with transformed data. Pipes are used to change date, currency, strings, and other pieces of information.

An example of a pipe is `date | shortdate` – this will convert the date object into a shorter date format like ‘dd/MM/yyyy’.

The difference between pure and impure pipes is that an impure pipe can produce different output for similar kinds of input, whereas a pure pipe produces similar output for the same input. Angular comes with certain built-in pipes; all of them are pure in nature.

A pure pipe reduces the recalculation of values. It returns a value when the input is different from the previous value.

A pure pipe triggers change detection only when one of its values changes and the view is updated with the new value passed to the pipe.

@Pipe({
   name: 'filterPipe', 
   pure: true     
 })
 export class FilterPipe {}

On the other hand, an impure pipe run changes detection cycles on every change, so they are not good performance wise.

Unsubscribing from Observables

Observables have a subscribe method which is called with a callback function in order to get the values emitted.

What if the subscription is not closed? The callback function attached to it will be continuously called, which could pose memory leaks and degrade performance.

It’s always a good practice to unsubscribe from observables using a lifecycle hook method called ‘OnDestroy’.

Web Workers

Web workers help to improve the performance of an Angular app by shifting some event-driven main thread tasks. Javascript is single-threaded; it has a main thread which runs in the browser. If our application is going to have some heavy tasks on startup, such as complex calculations or graph rendering, then it may freeze the UI and the user may get annoyed with the application. The solution to this problem is web workers. Web workers create a new thread that runs parallel to the main thread. You can see an example of this below:

   
if (typeof Worker !== 'undefined') {
        // Creating a new worker
        const worker = new Worker('./app.worker', { type: 'module' });
        worker.onmessage = ({ data }) => {
            console.log(`message: ${data}`);
        };
        worker.postMessage('Web workers at work…');
} else {
        // fallback mechanism so that your program still executes correctly
}

Web workers eliminate the inclusion of these complex processes in the main thread and run those in the background processes, resulting in the effortless operation of the UI. There are lots of use cases for web workers, including complex calculation, real-time content formatting, image filtering, and more.

Using TrackBy on ngFor

Angular uses the ngFor directive for iterating over data and manipulating the DOM by adding and removing elements. If it is not used properly, it may cause performance issues while rendering.

Consider a scenario where we are retrieving a list of cities and displaying all of the cities on the page. If the user adds a new city, then we have to fetch the list again from the server and reassign the value to the list variable, which causes re-rendering for that ‘ngFor’ directive. What we expected is the list becomes appended with only newly added items when a new collection is fetched. Here ‘ngFor’ is causing unnecessary re-rendering of DOM.

For such cases, there is no need to re-render the whole list – we just need to append the newly added item to the list. This can be done through the ‘ngForTrackBy’ option, which makes the ngFor directive rendering smarter.

What goes on behind the scenes is that `ngForTrackBy` accepts a function that returns the identity to compare with. So while rendering the template for each iteration, it stores a default value against each template, so next time before rendering the DOM it checks for the ngForTrackBy function value. If it is changed then, it will only re-render the template – otherwise the same old in-memory DOM will be kept. This is how it enhances the performance.

Optimizing Loading-time Performance

Now, let’s talk about how we can optimize the loading time of our applications by incorporating different loading-time optimization techniques.

Lazy Loading

While designing an application, we split up major functionalities into modules or sub-modules. Modules are good for maintenance and management of code. But imagine if our application is going to load all of them at startup. The code volume will increase and the app bundle size significantly grows. Loading large Javascript files in the browser impacts the initial load time. This is not optimal; apps should load fast – an ideal time for loading is about 3 seconds.

Lazy loading helps to boost the performance of your app. It is a default feature in Angular apps, which allows modules to be loaded only when they are required. Utilizing lazy loading also prevents redundant files from being loaded.

Using lazy loading, initial load times can be significantly reduced, as only the required modules are going to be loaded and not the entire app, which leads to reduced load time of the application.

Here is an example of lazy loading in Angular and JavaScript:

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

The ‘loadChildren’ option on a route is the place where we can mention a particular Angular ‘NgModule’ path, followed by ‘#ModuleName’. Based on the matched route, it loads the relevant bundle of an application. This ensures that what we are loading is the same as what we are seeing on the screen.

Server-side Rendering

Server-side Rendering – or SSR – is a classic technique. However, it is used in Angular applications to boost the load time of an app. It is used for rendering pages on the server side instead of the browser.

What happens Behind the Scenes?

When a request is sent to a server, the page is rendered on the server-side. The server then sends the full HTML back to the client. The client then directly renders it on the page. As soon as the client-side scripting is available, the SSR code starts working in the browser.

SSR is leveraged for primarily two reasons: Search Engine Optimization (SEO) and end-user performance benefits.

Preloading Modules

Lazy loading speeds up the loading time of our applications by fragmenting them into multiple bundles and loading them as requested.

One of the drawbacks of lazy loading is that when a user navigates to a lazy loadable part of our application, then the router has to fetch that required module from the server, which is a time-consuming process.

To fix this problem, preloading is introduced. With preloading, a router can easily preload lazy loadable modules in the background while the user is interacting with other parts of the application.

With this approach, initial load time is as small as it can be and navigations are instant.

So how does it work?

  • Load the initial bundle containing only the components needed to bootstrap our application.
  • Application is bootstrapped using small bundles.
  • The application is running. Users can start interacting with it; in the meantime other modules can be preloaded.
  • If user clicks on a lazy loadable module, the navigation will be instant.

Preloading can be enabled by passing a preloading strategy into forRoot, as shown in the following code example:

@NgModule({
  bootstrap: [ShopAppCmp],
  imports: [RouterModule.forRoot(ROUTES, 
    {preloadingStrategy: PreloadAllModules})]
})
class ShopModule {}

The latest version of router comes with two strategies: preload nothing and preload all modules; you can use your own choice depending on the application requirements.

Read more tutorials and guides on the Angular framework.

Final Thoughts on Angular App Optimization

Developing an Angular application is an easy task. The challenging task is to optimize its performance for a great end-user experience. Although there are many ways of improving the performance of an application, but incorporating some of the important optimization techniques inside your application can really help in optimizing and fine-tuning your Angular apps.

Tariq Siddiqui
Tariq Siddiqui
A graduate in MS Computer Applications and a Web Developer from India with diverse skills across multiple Web development technologies. Enjoys writing about any tech topic, including programming, algorithms and cloud computing. Traveling and playing video games are the hobbies that interest me most.

Get the Free Newsletter!

Subscribe to Developer Insider for top news, trends & analysis

Popular Articles

Featured