Post

Angular app optimization

Angular app optimization

Angular Application Optimizations Tips and Tricks.

Angular is a front-end framework that is used to build complex and dynamic web applications. Angular apps often start out small but grows in size over time and so does it’s bundle size and memory affecting it’s performance. Optimizing an angular application ensures that your app loads fast, has an efficient utilization of resources and scales effectively. The following are optimization techniques we can apply to improve the performance of an angular application:

1. Lazy loading Modules

In angular, modules(NgModules) acts as a container for grouping all the related components, directives, services and routes into a cohesive unit. Instead of putting all these into the root Module i.e AppModule, you should put them into feature specific modules. Using lazy loading, the feature modules will only be initialized when they are required. This decreases the main bundle size of the app as it loads only what the user needs to see first, making the app load faster. The following example shows a project that has a modular structure with 3 modules.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
src/
└── app/
    ├── app-routing.module.ts         # Routing configuration
    ├── app.module.ts                 # Root module
    ├── home/                         # Eagerly loaded
    │   ├── home.component.ts
    │   ├── home.component.html
    │   ├── home.module.ts
    │   └── home-routing.module.ts
    ├── dashboard/                    # Lazy-loaded module
    │   ├── dashboard.component.ts
    │   ├── dashboard.component.html
    │   ├── dashboard.module.ts
    │   └── dashboard-routing.module.ts
    └── users/                        # Lazy-loaded module
        ├── users.component.ts
        ├── users.component.html
        ├── users.module.ts
        └── users-routing.module.ts

The home component is eargerly loaded as it’s required to be shown to the users on the initial load. Dashboard and User Modules are only loaded when required. The following code snippet in the routing file shows how this is configured. The routing file can be within a submodule or in the app-routing.module.ts file.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
import { NgModule } from '@angular/core';
import { RouterModule, Routes } from '@angular/router';
import { HomeComponent } from './home/home.component';

const routes: Routes = [
  { path: '', component: HomeComponent },
  {
    path: 'dashboard',
    loadChildren: () =>
      import('./dashboard/dashboard.module').then(m => m.DashboardModule),
  },
  {
    path: 'users',
    loadChildren: () =>
      import('./users/users.module').then(m => m.UsersModule),
  },
  { path: '**', redirectTo: '' } // fallback route
];

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

Angular will load the module and it’s related components only when the route is reached, not in the initial load,therefore improving the performance.

2. Lazy Loading and Optimizing Images

Images often account for the largest chunk of data downloaded in an angular app. This can hurt the app’s performance by slowing it’s initial load time. Angular provides built in tools to optimize image delivery. You can lazy load the images, this means delaying the loading of images until they’re actually visible in the viewport (i.e when the user scrolls to them).

Native Html Support

1
<img src="assets/product.jpg" loading="lazy" alt="Product Image" />

Lazy loading is not enough, we should also optimize the images themselves: Follow the following tips to optimize your images:

i) Use modern image formats like WebP and AVIF. They are 25-30% smaller that .JPG and .PNG with decent quality. ii) Compress your images - Use online tools to compress your images. iii) Use CDN’s for Image Delivery - For large Angular applications, consider serving images from a CDN such as CloudFlare which loads images faster, auto convert and compress images automatically etc

3. Controlling Change Detection - Use onPush

Angular apps use change detection to update the DOM whenever something changes in the component, the Default change detection algorithm checks every component in the app on any change, even when only one part of the app has changed. Whenever something is changed in our application, such as a click event or a promise, Angular will compare the old and new values of an expression and decide whether the view should be updated. This default behavior is expensive but does not matter to us until the project grows. A good solution in using OnPush change detection. ChangeDetectionStrategy.OnPush tells angular “Only check this component (and its children) for updates if one of its input properties changes by reference.” This makes change detection smarter and faster, especially in large apps.

4. Tree Shaking and Bundle Size Elimination

Tree shaking is a build optimization tool that eliminates dead and unused code from your final bundle, Angular uses Webpack under the hood, a tool that bundles your application Js files for usage in the browser. When you run ng build –prod , it automatically runs tree shaking.This process is crucial for reducing the size of your application’s JavaScript files, leading to faster load times and improved overall performance. Use the following tricks to help angular’s tree-shaking eliminate unused code: i) Use AOT Compilation ii) Keep Modules and Services Small iii) Use Dependency Injection in services iv) Audit your code regularly - review your codebase to identify and remove unused components, services and modules.

Bundle size elimination tips:

i) Use Specific Imports (Avoid Whole Libraries)

Avoid

1
import * as _ from 'lodash';

Use

1
import cloneDeep from 'lodash/cloneDeep';

ii) Remove Unused Angular Features/Modules Only import what is needed

1
2
3
4
5
// Bad (imports unnecessary features)
import { FormsModule, ReactiveFormsModule } from '@angular/forms';

// Good
import { ReactiveFormsModule } from '@angular/forms';

iii) DevDependencies

In your package.json, dependencies are split into two categories: dependencies and devDependencies. dependencies are included in the production bundle while devDependencies are not. When a package is used for things like compiling, linting, testing keep it under devDependencies.

5. Use trackBy in ngFor loops

By default, Angular’s *ngFor directive re-renders all the items in a list whenever the data changes, even if only one item has changed. This is because angular tracks items by object reference. When a new array/list is passed, for example in an API call, all the references changes. This leads to unnecessary DOM manipulations and poor perfomance is large lists. However, this can be solved by use of the trackBy property that keeps the unchanged items in the DOM and only updates the one that have changed.

1
2
3
4
5
6
7
8
9
10
<!-- Before -->
<div *ngFor="let product of products">
  
</div>

<!-- After -->
<div *ngFor="let product of products; trackBy: trackByProductId">
  
</div>

Track by the unique identifier.

1
2
3
trackByProductId(index: number, product: Product): string {
  return product.id;
}

You can combine trackBy with OnPush change detection for lightning-fast rendering.

6. Avoid Memory Leaks

A memory leaks occurs when your app retains memory that it no longer needs which prevents the javascript run time engine from garbage collecting it. Over time, the leaks accumulate which leads to sluggish performance, increased memory usage and eventually the browser crashing. Memory leaks occurs when:

i) Subscriptions aren’t unsubscribed In the following example, This subscription lives on forever, even if the component is destroyed.

1
2
3
4
5
ngOnInit() {
  this.userService.getUsers().subscribe(users => {
    this.users = users;
  });
}

The solution is to unsubscribe properly - Angular 16+

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
import { Component, inject } from '@angular/core';
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';

@Component({
  selector: 'app-your-component',
  templateUrl: './your-component.component.html',
})
export class YourComponent {
  private destroyRef = inject(DestroyRef);

  users: any[] = [];

  constructor(private userService: UserService) {}

  ngOnInit() {
    this.userService.getUsers()
      .pipe(takeUntilDestroyed(this.destroyRef))
      .subscribe(users => {
        this.users = users;
      });
  }
}

ii) Remove event listeners Event handler remains in memory even after the component is destroyed.

1
2
3
4
5
6
7
ngOnInit() {
  window.addEventListener('resize', this.onResize);
}

ngOnDestroy() {
  window.removeEventListener('resize', this.onResize);
}

7. Use Immutable Data

An immutable data structure is one that cannot be changed once created. Angular uses immutable data structures by default. This means that whenever a change to the data structure, a new copy is made. Angular’s change detection relies heavily on object references to know when something has changed. When you mutate an object directly, Angular might not detect the change efficiently, which could result in Unnecessary re-renders, Missed updates in the UI and poor performance especially in large apps. By using immutable data structures, angular can quickly and efficiently detect changes using reference check making your app faster.

8. Use a caching strategy

Caching is a powerful tool that can significantly improve your application performance. You don’t need to make API calls for frequently used data that does not change frequently, e.g a list of countries and it’s regions. This speeds up loading times and reduces strain on your back end. There are different methods of caching data: i) In memory caching - Store data within services during the lifecycle of the app.

ii) Local Storage and Session Storage - Good for caching simple, non-sensitive data across sessions.

iii) Indexed DB - For complex or large offline datasets

Always ensure your cached data stays fresh and relevant by setting data TTL (Time To Live). This Expire data after a certain period and makes a data fetch silently on the background.

  1. AOT AOT compilation is a technique that allows Angular to compile the application code ahead of time. This can help to improve performance by reducing the amount of work that needs to be done by the browser when the application is loaded. Since the compiler isn’t included in the bundle, and templates are already compiled to JS, the browser has less work to do, resulting in faster boot time.

AOT is enabled by default in production builds when using the Angular CLI:

1
ng build --configuration production

You can also explicitly enable it in your angular.json:

1
2
3
4
5
"configurations": {
  "production": {
    "aot": true
  }
}

10. Use pure pipes instead of methods.

When we use a method in our template to do some calculations, the change detection will be triggered to re-render the component more frequently. This will affect the performance when there are many interactions in the template and when the processing is heavy. Pure pipes is a good solution for this issue since it is called only when function parameters change.

Conclusion

Optimizing an angular application ensures that your app loads fast, has an efficient utilization of resources and scales effectively. Remember, performance is not a one-time fix but an ongoing mindset. Start small, optimize incrementally and measure impact along the way.

This post is licensed under CC BY 4.0 by the author.