DEV Community

Renuka Patil
Renuka Patil

Posted on

7.Performance Optimization: Understanding Change Detection(Zone.js | default | onPush), Lazy Loading, Track By in ngFor

We will cover these following topics in this blog:

  1. What is Zone.js?
  2. What is Change Detection?
  3. Change Detection Strategies: default and onPush.
  4. Advanced Concepts in Change Detection: KeyValueDiffer, IterableDiffer.
  5. Interview Questions on Change Detection

What is Zone.js?

Zone.js is a library that provides an execution context for JavaScript code. It allows tracking of asynchronous operations like HTTP requests, timers, and other events. Zone.js helps Angular know when an asynchronous operation (like an API call or user input) has finished, and thus, it can trigger Angular's Change Detection to update the view.

In Angular, Zone.js is used to automatically trigger the change detection cycle whenever asynchronous operations (like HTTP requests, setTimeout, etc.) are completed. Without Zone.js, Angular wouldn't know that something async (like an HTTP request) has finished, and thus, wouldn't know that it needs to check for changes in the model and update the view.


How Zone.js Works with Angular

When you perform any asynchronous operation in Angular, such as making an HTTP request, Zone.js monitors the execution context. Once the asynchronous operation completes, Zone.js notifies Angular that it needs to run the Change Detection Cycle to update the view with the new data.

Without Zone.js, Angular would not automatically detect these asynchronous changes, and the UI would not be updated in response to these operations.


Example of Zone.js in Action

Let's see how Zone.js interacts with Angular's Change Detection in an HTTP call scenario.

Code Example: Zone.js with Change Detection

import { Component, OnInit } from '@angular/core';
import { HttpClient } from '@angular/common/http';

@Component({
  selector: 'app-http-example',
  template: `
    <h2>Users List</h2>
    <ul>
      <li *ngFor="let user of users">{{ user.name }}</li>
    </ul>
    <button (click)="loadUsers()">Load Users</button>
  `,
})
export class HttpExampleComponent implements OnInit {
  users: any[] = [];

  constructor(private http: HttpClient) {}

  ngOnInit(): void {}

  loadUsers() {
    this.http.get<any[]>('https://jsonplaceholder.typicode.com/users')
      .subscribe(data => {
        this.users = data; // Zone.js will track this async operation
        // Angular's Change Detection will automatically run after the HTTP request completes
      });
  }
}
Enter fullscreen mode Exit fullscreen mode

Explanation:

  • We use the HttpClient to fetch data from a remote API.
  • Zone.js tracks the asynchronous operation (the HTTP request).
  • Once the HTTP request completes and data is returned, Zone.js triggers Angular's Change Detection Cycle, updating the view with the newly fetched user data.

In the absence of Zone.js, Angular would not be aware that an asynchronous operation has finished, and thus, the users array wouldn't be reflected in the UI.


Zone.js and Its Impact on Performance

Zone.js provides an automatic and transparent way to handle asynchronous operations, but it can also have performance overhead if not managed properly. The automatic change detection on every async operation could be expensive in complex applications with lots of asynchronous operations happening frequently.

To optimize performance, Angular provides the OnPush change detection strategy, which reduces the number of checks Angular has to perform. Even with Zone.js, Angular can still perform more efficiently when using OnPush, as it avoids checking components unnecessarily.


Why Does Angular Use Zone.js?

Angular uses Zone.js because it simplifies the task of automatically detecting changes in a complex application. By wrapping async operations and notifying Angular when to check for changes, Zone.js ensures that developers don't have to manually manage the triggering of change detection after each asynchronous operation.


Summary of Zone.js in Angular

  • Zone.js is a library that allows Angular to track asynchronous operations like HTTP requests, user inputs, etc.
  • It triggers Angular's Change Detection cycle automatically whenever an asynchronous operation completes, ensuring the UI stays in sync with the model.
  • Without Zone.js, Angular would not be able to detect changes triggered by asynchronous events (like HTTP calls), and the view would not update automatically.
  • Zone.js helps Angular maintain automatic change detection, but developers can fine-tune performance using strategies like OnPush to minimize the impact of frequent change detection cycles.

Understanding Change Detection

In Angular, Change Detection is the mechanism that keeps the user interface (UI) in sync with the data model. Whenever the data changes, Angular automatically updates the UI to reflect those changes. This process is crucial in ensuring that the application responds to dynamic user inputs and data updates.

In this blog, we'll go deep into the concept of Change Detection, explain its various strategies, and provide code examples along with explanations. Additionally, we will discuss interview questions around this topic to help you better prepare.


What is Change Detection?

Change Detection is the process where Angular checks the state of the model and updates the view (UI) whenever the model changes. Whenever a change occurs, Angular runs its Change Detection Cycle, and updates the view to reflect the latest data.

In Angular, this is handled automatically for most scenarios, but there are different strategies that Angular uses to optimize performance and handle changes more efficiently.

Key Concepts

  1. Zone.js:
    Angular uses Zone.js to track asynchronous operations. Whenever something happens asynchronously (like an API call), Zone.js ensures that the change detection cycle is triggered to update the UI.

  2. Change Detection Cycle:
    When data changes (whether manually or via async operations like API calls), Angular triggers the change detection cycle to update the UI. The cycle checks the state of the model, compares it with the view, and updates the view if necessary.

Change Detection Strategies

Angular provides two main strategies to optimize the change detection mechanism:

  1. Default Change Detection:
    This is the default strategy. Whenever any data bound to the view changes, Angular checks the entire component tree to see if there are any changes. It will update the UI accordingly.

  2. OnPush Change Detection:
    This strategy tells Angular to check the component only when certain conditions are met:

    • When an input property of the component changes.
    • When an event is triggered in the component.

Why Change Detection is Important?

Change Detection is crucial because it keeps the view and the model synchronized. Without it, the UI would not update, leading to outdated or inconsistent data being displayed.


Angular Change Detection Code Examples

1. Default Change Detection

In this example, Angular uses the default change detection strategy, where the UI is automatically updated whenever the data changes.

Code Example:
import { Component } from '@angular/core';

@Component({
  selector: 'app-user-list',
  template: `
    <h2>User List</h2>
    <ul>
      <li *ngFor="let user of users">{{ user.name }}</li>
    </ul>
    <button (click)="addUser()">Add User</button>
  `,
})
export class UserListComponent {
  users = [
    { name: 'John Doe' },
    { name: 'Jane Doe' },
  ];

  addUser() {
    this.users.push({ name: 'New User' });
  }
}
Enter fullscreen mode Exit fullscreen mode
Explanation:

In this example:

  • We have an array of users displayed as a list.
  • On clicking the button, a new user is added to the array.
  • Since Angular uses the default change detection, when the array (users) is modified, Angular will automatically check and update the UI to show the new user.
Output:

Initially:

User List
- John Doe
- Jane Doe
Enter fullscreen mode Exit fullscreen mode

After clicking the Add User button:

User List
- John Doe
- Jane Doe
- New User
Enter fullscreen mode Exit fullscreen mode

2. OnPush Change Detection

In the OnPush strategy, Angular only checks the component if one of the following occurs:

  • Input data changes: Angular checks the component when an @Input property changes.
  • Event handler triggers: Angular triggers change detection when a user action, like a button click, happens.
Code Example:
import { Component, ChangeDetectionStrategy } from '@angular/core';

@Component({
  selector: 'app-user-list',
  template: `
    <h2>User List</h2>
    <ul>
      <li *ngFor="let user of users">{{ user.name }}</li>
    </ul>
    <button (click)="addUser()">Add User</button>
  `,
  changeDetection: ChangeDetectionStrategy.OnPush
})
export class UserListComponent {
  users = [
    { name: 'John Doe' },
    { name: 'Jane Doe' },
  ];

  addUser() {
    this.users = [...this.users, { name: 'New User' }];
  }
}
Enter fullscreen mode Exit fullscreen mode
Explanation:

In this example:

  • We set the changeDetection strategy to OnPush.
  • When we modify the users array directly, Angular will not update the UI. This is because OnPush only checks for changes when:
    • The input properties of the component change.
    • An event like a button click is triggered.
  • By using the spread operator (...), we create a new reference for the users array, which triggers the change detection manually.
Output:

Initially:

User List
- John Doe
- Jane Doe
Enter fullscreen mode Exit fullscreen mode

After clicking the Add User button:

User List
- John Doe
- Jane Doe
- New User
Enter fullscreen mode Exit fullscreen mode

Advanced Concepts in Change Detection

  1. KeyValueDiffer: This is used when you want to detect changes in an object's properties.

  2. IterableDiffer: This is used to detect changes in an iterable (like an array).

  3. Manually Triggering Change Detection:
    If you're using the OnPush strategy and need to manually trigger change detection, you can use ChangeDetectorRef.

Code Example: Manually Triggering Change Detection

import { Component, ChangeDetectorRef } from '@angular/core';

@Component({
  selector: 'app-user-list',
  template: `
    <h2>User List</h2>
    <ul>
      <li *ngFor="let user of users">{{ user.name }}</li>
    </ul>
    <button (click)="addUser()">Add User</button>
  `,
})
export class UserListComponent {
  users = [
    { name: 'John Doe' },
    { name: 'Jane Doe' },
  ];

  constructor(private cdRef: ChangeDetectorRef) {}

  addUser() {
    this.users.push({ name: 'New User' });
    this.cdRef.detectChanges();  // Manually trigger change detection
  }
}
Enter fullscreen mode Exit fullscreen mode

Interview Questions on Change Detection

  1. What is Change Detection in Angular?

    • Answer: Change Detection is the mechanism in Angular that ensures the view is in sync with the model. Whenever a data change occurs, Angular updates the UI automatically to reflect the changes.
  2. What is the difference between the Default and OnPush Change Detection strategies?

    • Answer:
      • The Default strategy checks for changes every time any event occurs, including async operations.
      • The OnPush strategy checks for changes only when the input properties change or when an event (like a button click) triggers a check.
  3. How does Zone.js work with Angular Change Detection?

    • Answer: Zone.js is a library that helps Angular detect changes by wrapping async operations (like HTTP calls or timers). It notifies Angular when an async operation finishes and triggers the change detection cycle.
  4. How can you manually trigger change detection?

    • Answer: You can use the ChangeDetectorRef service to manually trigger change detection by calling its detectChanges() method.
  5. What is the purpose of ChangeDetectorRef in Angular?

    • Answer: ChangeDetectorRef allows you to manually control the change detection mechanism. You can use it to trigger or stop change detection for specific components.
  6. What is the significance of the OnPush strategy?

    • Answer: The OnPush strategy optimizes performance by reducing the number of change detection checks. It triggers a check only when input properties change or an event occurs.
  7. How does Angular detect changes in arrays or objects?

    • Answer: Angular uses IterableDiffer for arrays and KeyValueDiffer for objects. These classes compare the current and previous values to detect changes.

Conclusion

Change Detection in Angular is a crucial concept for keeping the UI in sync with the model. Understanding the different strategies like Default and OnPush can help optimize performance, especially in large applications. By manually triggering change detection when needed, you can have finer control over how and when the UI updates.

By following the examples provided, you can experiment with different strategies and understand how Angular performs Change Detection under various scenarios.


Module Loading Strategies: Eager Loading, Lazy Loading, and Preloading

Module Loading Strategy

  1. Eager loading
  2. Lazy loading
  3. Pre laoding

Angular applications are often modular, consisting of multiple feature modules that can be loaded into the application dynamically or statically. Optimizing how these modules are loaded is crucial for performance and user experience. Angular provides three primary loading strategies: Eager Loading, Lazy Loading, and Preloading. In this blog, we’ll explore each strategy with examples.


1. Eager Loading

Eager Loading is the default module loading strategy in Angular. Here, all the modules are loaded at the application's initialization. While this ensures all functionality is ready upfront, it can negatively impact performance, especially for large applications, by increasing the initial load time.

Example of Eager Loading

Let’s assume we have two feature modules: DashboardModule and AdminModule.

app.module.ts

import { NgModule } from '@angular/core';
import { BrowserModule } from '@angular/platform-browser';
import { AppRoutingModule } from './app-routing.module';
import { AppComponent } from './app.component';
import { DashboardModule } from './dashboard/dashboard.module';
import { AdminModule } from './admin/admin.module';

@NgModule({
  declarations: [
    AppComponent
  ],
  imports: [
    BrowserModule,
    AppRoutingModule,
    DashboardModule, // Eagerly loaded
    AdminModule       // Eagerly loaded
  ],
  bootstrap: [AppComponent]
})
export class AppModule { }
Enter fullscreen mode Exit fullscreen mode

When to Use Eager Loading

  • Small applications.
  • Modules with critical functionality required at the start.

2. Lazy Loading

Lazy Loading allows modules to be loaded only when they are required. This reduces the application's initial load time, enhancing performance for larger applications.

How to Implement Lazy Loading

Let’s lazy load the AdminModule using Angular's route configuration.

app-routing.module.ts

import { NgModule } from '@angular/core';
import { RouterModule, Routes } from '@angular/router';

const routes: Routes = [
  { 
    path: 'admin', 
    loadChildren: () => import('./admin/admin.module').then(m => m.AdminModule) 
  }
];

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

admin.module.ts

import { NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';
import { RouterModule, Routes } from '@angular/router';
import { AdminComponent } from './admin.component';

const routes: Routes = [
  { path: '', component: AdminComponent }
];

@NgModule({
  declarations: [AdminComponent],
  imports: [
    CommonModule,
    RouterModule.forChild(routes)
  ]
})
export class AdminModule { }
Enter fullscreen mode Exit fullscreen mode

Now, the AdminModule will load only when the user navigates to the /admin route.

When to Use Lazy Loading

  • Large applications with many feature modules.
  • Features that are rarely accessed.

3. Preloading

Preloading combines the benefits of both eager and lazy loading by loading non-critical modules in the background after the application has been initialized. Angular provides the PreloadAllModules strategy to load all lazily loaded modules in the background.

How to Implement Preloading

app-routing.module.ts

import { NgModule } from '@angular/core';
import { RouterModule, Routes } from '@angular/router';
import { PreloadAllModules } from '@angular/router';

const routes: Routes = [
  { 
    path: 'admin', 
    loadChildren: () => import('./admin/admin.module').then(m => m.AdminModule) 
  },
  { 
    path: 'dashboard', 
    loadChildren: () => import('./dashboard/dashboard.module').then(m => m.DashboardModule) 
  }
];

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

When to Use Preloading

  • Applications with medium to large modules.
  • Features that are not immediately required but frequently accessed.

Comparing Loading Strategies

Strategy Pros Cons Use Case
Eager Loading Simple to implement, ready at startup. Increases initial load time. Small apps or critical modules.
Lazy Loading Reduces initial load time, better UX. Adds complexity to routing. Large apps or rarely accessed features.
Preloading Combines benefits of both strategies. Still requires careful planning. Apps needing a balance of performance and UX.

Conclusion

Choosing the right module loading strategy is essential for optimizing Angular applications. Eager loading works for smaller apps, lazy loading for large-scale apps, and preloading for a balanced approach. By understanding these strategies and their use cases, you can significantly enhance your application's performance and user experience.


Optimizing Angular Lists with trackBy in ngFor

When working with lists in Angular, the ngFor directive is commonly used to loop through collections. However, when a list updates, Angular's default behavior is to re-render the entire list, even if only one item changes. This can lead to performance issues in large lists. The trackBy option in ngFor solves this problem by helping Angular identify which items have changed, added, or removed, minimizing DOM manipulations.


Understanding trackBy

The trackBy function provides a unique identifier for each item in the list, allowing Angular to efficiently manage the DOM. Without it, Angular relies on object references, which can result in unnecessary re-renders.

Syntax

<div *ngFor="let item of items; trackBy: trackByFn">
  {{ item.name }}
</div>
Enter fullscreen mode Exit fullscreen mode

Why Use trackBy?

Consider the following scenario: You have a list of items, and you update one item's content. Without trackBy, Angular will destroy and recreate the entire list, even if only one item changes.

Example Without trackBy

app.component.ts

import { Component } from '@angular/core';

@Component({
  selector: 'app-root',
  template: `
    <button (click)="updateList()">Update List</button>
    <ul>
      <li *ngFor="let item of items">{{ item.name }}</li>
    </ul>
  `
})
export class AppComponent {
  items = [
    { id: 1, name: 'Item 1' },
    { id: 2, name: 'Item 2' },
    { id: 3, name: 'Item 3' }
  ];

  updateList() {
    // Only updates the name of the first item
    this.items[0].name = 'Updated Item 1';
  }
}
Enter fullscreen mode Exit fullscreen mode

When updateList is called, Angular re-renders the entire list, even though only one item's name has changed.


Improving Performance with trackBy

By adding a trackBy function, Angular will only update the changed item.

Updated Example with trackBy

app.component.ts

import { Component } from '@angular/core';

@Component({
  selector: 'app-root',
  template: `
    <button (click)="updateList()">Update List</button>
    <ul>
      <li *ngFor="let item of items; trackBy: trackById">{{ item.name }}</li>
    </ul>
  `
})
export class AppComponent {
  items = [
    { id: 1, name: 'Item 1' },
    { id: 2, name: 'Item 2' },
    { id: 3, name: 'Item 3' }
  ];

  updateList() {
    this.items[0].name = 'Updated Item 1';
  }

  trackById(index: number, item: any): number {
    return item.id; // Return a unique identifier for the item
  }
}
Enter fullscreen mode Exit fullscreen mode

Key Points

  1. The trackById function returns a unique identifier (id) for each item.
  2. Angular uses this identifier to determine which items have changed.

Benefits of Using trackBy

  1. Improved Performance: Reduces DOM manipulations by only updating changed items.
  2. Better Scalability: Handles large lists efficiently.
  3. Accurate State Preservation: Ensures that state (e.g., input focus, animations) is preserved for unchanged items.

Common Use Cases

  1. Lists with Unique Identifiers: Use trackBy when items have unique ids or keys, such as data from a database.
  2. Dynamic Data: When lists are frequently updated, such as real-time data streams or API responses.
  3. Reusable Components: Optimize performance in components dealing with large or complex lists.

Conclusion

Using trackBy in ngFor is a simple yet powerful optimization that enhances the performance of Angular applications. By leveraging unique identifiers, Angular minimizes unnecessary DOM operations, ensuring a smoother and more efficient user experience.


AOT Compilation in Angular: Boosting Performance with Pre-compiled Templates

Angular applications can be compiled in two ways: JIT (Just-in-Time) and AOT (Ahead-of-Time). AOT compilation enhances performance by pre-compiling Angular HTML templates and TypeScript code into JavaScript before the browser downloads and executes the application.

This blog will dive into AOT compilation, its benefits, and how to enable it in your Angular projects.


What is AOT Compilation?

In the default JIT mode, Angular templates are compiled in the browser at runtime. While convenient during development, this approach introduces performance drawbacks for production applications.

AOT compilation, on the other hand, processes the templates and components during the build phase. This means the browser only executes the pre-compiled JavaScript code, resulting in faster load times and fewer runtime errors.


Benefits of AOT Compilation

  1. Improved Performance

    • Reduces the size of Angular frameworks by removing runtime compilation libraries.
    • Faster rendering because templates are pre-compiled.
  2. Early Error Detection

    • Identifies template binding issues during the build process instead of at runtime.
  3. Enhanced Security

    • Templates are converted into JavaScript code, which avoids runtime interpretation and minimizes security risks like injection attacks.
  4. SEO and Server-Side Rendering

    • Pre-compiled templates improve the rendering process, making server-side rendering and SEO optimization more efficient.

How to Enable AOT Compilation

AOT is enabled by default in Angular's production builds. You can also manually enable it during development.

1. Using the Angular CLI for Production

When building for production, AOT is automatically enabled:

ng build --prod
Enter fullscreen mode Exit fullscreen mode

2. Enabling AOT in Development

You can force AOT compilation during development:

ng build --aot
Enter fullscreen mode Exit fullscreen mode

or

ng serve --aot
Enter fullscreen mode Exit fullscreen mode

Behind the Scenes: How AOT Works

  1. Template Parsing: Angular templates are parsed into an Abstract Syntax Tree (AST).
  2. Template Type Checking: TypeScript checks are applied to ensure bindings are valid.
  3. Code Generation: Templates are compiled into efficient JavaScript code.
  4. Tree Shaking: Unused code is removed, reducing the bundle size.

AOT vs. JIT Compilation

Feature AOT Compilation JIT Compilation
Compilation Time Build time (before running in the browser). Runtime (in the browser).
Performance Faster application startup. Slower startup due to runtime compilation.
Error Detection Detects errors during build time. Errors might appear at runtime.
Use Case Recommended for production. Suitable for development/debugging.

AOT in Action: Example

Component Template

<div>{{ user.name }}</div>
Enter fullscreen mode Exit fullscreen mode

Component Class

export class AppComponent {
  user = { name: 'John Doe' };
}
Enter fullscreen mode Exit fullscreen mode

AOT Output

During AOT, the above template is compiled into JavaScript:

const user = { name: 'John Doe' };
const div = document.createElement('div');
div.textContent = user.name;
document.body.appendChild(div);
Enter fullscreen mode Exit fullscreen mode

The browser processes this JavaScript directly, avoiding the need for runtime template parsing.


Best Practices for AOT

  1. Avoid Dynamic Template Expressions

    • Avoid using functions or complex logic in template bindings.
    • Prefer:
     <div>{{ user.name }}</div>
    

    Over:

     <div>{{ getUserName() }}</div>
    
  2. Static Annotations

    • Use Angular decorators like @Component and @Injectable.
  3. Strict Type Checking

    • Ensure proper TypeScript typing to catch issues early.

Conclusion

AOT compilation is a powerful tool in Angular's arsenal to optimize application performance and security. By enabling pre-compilation of templates, AOT reduces runtime overhead and ensures a smoother user experience. Always leverage AOT for production builds to get the best out of your Angular applications.


You can see these blogs to cover all angular concepts:

Beginner's Roadmap: Your Guide to Starting with Angular

  1. Core Angular Concepts
  2. Services and Dependency Injection
  3. Routing and Navigation
  4. Forms in Angular
  5. RxJS and Observables
  6. State Management
  7. Performance Optimization

Have thoughts or questions? Let me know in the comments!

Top comments (1)

Collapse
 
jangelodev profile image
João Angelo

Hi Renuka Patil,
Very nice and helpful !
Thanks for sharing.