10th June 2024

Asynchrous and synchronous, Lazy Loading in Angular

Async-and-Sync-Image-VS-Online-img

Introduction

In the world of web development, particularly when working with Angular, it is essential to grasp the concepts of synchronous and asynchronous programming. These paradigms define how tasks are executed and managed, impacting the overall performance and user experience of an application.

Synchronous Programming in Angular

Synchronous programming in Angular follows the traditional, sequential execution model where each operation waits for the previous one to complete before proceeding. This approach is simple and predictable but can become inefficient when dealing with time-consuming tasks. Let's explore synchronous programming in Angular in greater detail with code snippets to illustrate key concepts.

Basic Synchronous Execution

In a synchronous context, operations are executed one after another. Let's start with a simple

example:
Example 1: Sequential Function Calls
                                
                                    
    function firstFunction() {
      console.log('First function executed');
    }

    function secondFunction() {
      console.log('Second function executed');
    }

    firstFunction();
    secondFunction();
  
                                
                            
Output:
                                
                                    
    First function executed
    Second function executed
  
                                
                            

In this example, secondFunction will not execute until firstFunction has completed.

Synchronous Operations in Angular Components

In Angular, component lifecycle hooks can be used to execute synchronous code. Let's look at an example where we initialize some data in the ngOnInit lifecycle hook.

Example 2: Synchronous Initialization in ngOnInit
                                
                                    
    import { Component, OnInit } from '@angular/core';

    @Component({
      selector: 'app-sync-example',
      template: '
        <div>
          <p>{{ message }}</p>
        </div>
      '
    })
    export class SyncExampleComponent implements OnInit {
      message: string;

      constructor() {
        this.message = '';
      }

      ngOnInit() {
        this.initializeData();
      }

      initializeData() {
        this.message = 'Data initialized synchronously';
        console.log(this.message);
      }
    }
  
                                
                            
Output in Console:
                                
                                    
    Data initialized synchronously
  
                                
                            

Here, the initializeData method is called synchronously within the ngOnInit hook, ensuring the data is set before the component is rendered.

Synchronous Services

Services in Angular can also perform synchronous operations. Consider a simple logging service:

Example 3: Synchronous Logging Service
                                
                                    
  import { Injectable } from '@angular/core';

    @Injectable({
      providedIn: 'root'
    })
    export class LoggingService {
      log(message: string) {
        console.log(Log message: $"{message}");
      }
    }
 
                                
                            

This logging service can be used synchronously in any component:

                                
                                    
    import { Component, OnInit } from '@angular/core';
    import { LoggingService } from './logging.service';

    @Component({
      selector: 'app-log-example',
      template: '
        <div>
          <p>Check console for log messages</p>
        </div>
      '
    })
    export class LogExampleComponent implements OnInit {

      constructor(private loggingService: LoggingService) {}

      ngOnInit() {
        this.performTask();
      }

      performTask() {
        this.loggingService.log('Task started');
        // Simulate task
        this.loggingService.log('Task completed');
      }
    }
 
                                
                            
Output in Console:
                                
                                    
    Log message: Task started
    Log message: Task completed

                                
                            
Potential Issues with Synchronous Programming

While synchronous programming is straightforward, it can lead to issues, particularly with long-running tasks. Let's consider an example where a synchronous operation blocks the UI:

Example 4: Blocking Operation
                                
                                    
  import { Component } from '@angular/core';

  @Component({
    selector: 'app-blocking-example',
    template: '
      <button (click)="performBlockingTask()">Run Blocking Task</button>
    '
  })
  export class BlockingExampleComponent {
    
    performBlockingTask() {
      console.log('Task started');
      // Simulate a blocking task
      const start = Date.now();
      while (Date.now() - start < 5000) {
        // Busy wait for 5 seconds
      }
      console.log('Task completed');
    }
  }

                                
                            
Output in Console:
                                
                                    
  Task started
  Task completed (after 5 seconds delay)

                                
                            

In this example, the performBlockingTask method simulates a blocking operation by busy-waiting for 5 seconds. During this time, the UI is unresponsive, illustrating why long-running tasks should generally be handled asynchronously.

Asynchronous Programming in Angular

Asynchronous programming in Angular allows for tasks to be performed concurrently, enabling the application to remain responsive while waiting for operations like data fetching or timers to complete. This approach is essential for handling real-world scenarios where blocking the main execution thread would lead to a poor user experience. Angular primarily uses Promises and Observables (from the RxJS library) for asynchronous operations.

Promises

A Promise represents a value that may be available now, or in the future, or never. Promises are often used for handling asynchronous operations in a simple and readable way.

Example 1: Basic Promise
                                
                                    
  function asyncFunction(): Promise<void> {
    return new Promise((resolve) => {
      setTimeout(() => {
        console.log('Async function executed');
        resolve();
      }, 1000);
    });
  }

  asyncFunction().then(() => {
    console.log('Then block executed');
  });

                                
                            
Output:
                                
                                    
  Async function executed
  Then block executed

                                
                            

In this example, asyncFunction returns a Promise that resolves after 1 second. The .then() block is executed once the Promise is resolved.

Observables

Observables, a core feature of RxJS, provide a powerful way to handle asynchronous data streams. They offer more functionality than Promises and are extensively used in Angular, particularly for HTTP requests and event handling.

Example 2: Basic Observable
                                
                                    
  import { Observable } from 'rxjs';

  const observable = new Observable(subscriber => {
    subscriber.next('First data');
    setTimeout(() => {
      subscriber.next('Second data');
      subscriber.complete();
    }, 1000);
  });

  observable.subscribe({
    next(data) { console.log(data); },
    complete() { console.log('Observable completed'); }
  });

                                
                            
Output:
                                
                                    
  First data
  Second data
  Observable completed

                                
                            

In this example, the Observable emits "First data" immediately and "Second data" after a 1-second delay, followed by a completion message.

Asynchronous Operations in Angular Components

Angular components frequently deal with asynchronous operations, such as fetching data from a server. The HttpClient service returns Observables, making it easy to handle these operations.

Example 3: HTTP GET Request with HttpClient

First, ensure that HttpClientModule is imported in your Angular module:

                                
                                    
  import { HttpClientModule } from '@angular/common/http';

  @NgModule({
    declarations: [
      // your components here
    ],
    imports: [
      HttpClientModule,
      // other modules here
    ],
    providers: [],
    bootstrap: [AppComponent]
  })
  export class AppModule { }

                                
                            

Then, use the HttpClient service in a component to make an HTTP GET request:

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

  @Component({
    selector: 'app-data',
    template: '
      <div *ngIf="data">
        <pre>{{ data | json }}</pre>
      </div>
    '
  })
  export class DataComponent implements OnInit {
    data: any;

    constructor(private http: HttpClient) {}

    ngOnInit() {
      this.fetchData();
    }

    fetchData() {
      this.http.get('https://jsonplaceholder.typicode.com/posts/1')
        .subscribe(response => {
          this.data = response;
          console.log('Data received:', this.data);
        });
    }
  }

                                
                            

In this example, an HTTP GET request is made when the component initializes (ngOnInit), and the data is displayed in the template once the response is received.

Using Async/Await

The async/await syntax provides a way to work with Promises more comfortably. It allows asynchronous code to be written in a synchronous style, making it easier to read and maintain.

Example 4: Async/Await
                                
                                    
  async function asyncFunction(): Promise<void> {
    console.log('Async function started');
    await new Promise(resolve => setTimeout(resolve, 1000));
    console.log('Async function executed');
  }

  async function run() {
    await asyncFunction();
    console.log('Then block executed');
  }

  run();

                                
                            
Output:
                                
                                    
  Async function started
  Async function executed
  Then block executed

                                
                            

In this example, the asyncFunction uses await to pause execution until the Promise is resolved, making the code easier to follow.

Asynchronous Error Handling

Handling errors in asynchronous operations is crucial for robust applications. Both Promises and Observables provide mechanisms for error handling.

Example 5: Error Handling with Promises
                                
                                    
  function asyncFunction(): Promise<void> {
    return new Promise((resolve, reject) => {
      setTimeout(() => {
        reject('Error occurred');
      }, 1000);
    });
  }

  asyncFunction()
    .then(() => {
      console.log('Then block executed');
    })
    .catch(error => {
      console.error('Catch block executed:', error);
    });

                                
                            
Output:
                                
                                    
  Catch block executed: Error occurred

                                
                            
Example 6: Error Handling with Observables
                                
                                    
  import { Observable, throwError } from 'rxjs';
  import { catchError } from 'rxjs/operators';

  const observable = new Observable(subscriber => {
    subscriber.error('Error occurred');
  });

  observable.pipe(
    catchError(error => {
      console.error('Catch block executed:', error);
      return throwError(error);
    })
  ).subscribe({
    next(data) { console.log(data); },
    error(err) { console.error('Error in subscription:', err); }
  });

                                
                            
Output:
                                
                                    
  Catch block executed: Error occurred
  Error in subscription: Error occurred

                                
                            

Lazy Loading

Lazy loading is a design pattern commonly used in web development to defer the loading of resources until they are needed. This technique improves the initial load time of web applications, reduces unnecessary resource usage, and enhances overall performance, especially in applications with large codebases or heavy content.

Benefits of Lazy Loading
  • Performance Optimization: By loading only the necessary resources initially, lazy loading reduces the initial load time and bandwidth usage.
  • Improved User Experience: Faster load times lead to a better user experience, as users can start interacting with the application sooner.
  • Efficient Resource Management: Lazy loading helps manage resources more efficiently, loading content only when needed and reducing the load on servers and client devices.

Implementing Lazy Loading in Angular

Lazy loading in Angular is typically achieved through route configuration. Here's a step-by-step guide to implementing lazy loading in an Angular application.

Setup Your Angular Project

Make sure you have the Angular CLI installed. Create a new Angular project using the CLI:

                                
                                    
  npm install -g @angular/cli
  ng new lazy-loading-demo
  cd lazy-loading-demo

                                
                            
Generate a Lazy-Loaded Module

Create a new module that will be loaded lazily:

                                
                                    
  ng generate module lazy --route lazy --module app.module

                                
                            

This command sets up a new module with its own route configured for lazy loading.

Configure Routes for Lazy Loading

In app-routing.module.ts, configure the lazy-loaded module route:

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

  const routes: Routes = [
    { path: '', redirectTo: '/home', pathMatch: 'full' },
    { path: 'home', component: HomeComponent },
    { path: 'lazy', loadChildren: () => import('./lazy/lazy.module').then(m => m.LazyModule) }
  ];

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

                                
                            

The loadChildren syntax dynamically imports the LazyModule only when the /lazy route is accessed.

Create Components in the Lazy Module

Generate components inside the lazy-loaded module:

                                
                                    
  ng generate component lazy/lazy-component

                                
                            
Set Up Routing in the Lazy Module

Configure the routes within lazy-routing.module.ts:

                                
                                    
  import { NgModule } from '@angular/core';
  import { RouterModule, Routes } from '@angular/router';
  import { LazyComponent } from './lazy-component/lazy-component.component';

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

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

                                
                            
Verify Your Setup
Run the application:
                                
                                    
  ng serve

                                
                            

Navigate to http://localhost:4200/lazy to see the lazy-loaded component in action.

Common Use Cases for Lazy Loading
  • Large Applications: For applications with many features and modules, lazy loading can significantly reduce the initial load time.
  • Media Content: Loading images, videos, or other media content only when they are in the viewport.
  • Third-Party Libraries: Defer loading of heavy third-party libraries until they are required.
Tools and Strategies for Lazy Loading
  • Angular Preloading Strategies: Use Angular’s built-in preloading strategies like PreloadAllModules to balance between eager and lazy loading.
                                
                                    
  imports: [RouterModule.forRoot(routes, { preloadingStrategy: PreloadAllModules })]

                                
                            
  • Intersection Observer API: For non-Angular projects or specific elements like images, you can use the Intersection Observer API to implement lazy loading.
                                
                                    
  const imgObserver = new IntersectionObserver((entries, observer) => {
    entries.forEach(entry => {
      if (entry.isIntersecting) {
        const img = entry.target;
        img.src = img.dataset.src;
        observer.unobserve(img);
      }
    });
  });

  document.querySelectorAll('img[data-src]').forEach(img => {
    imgObserver.observe(img);
  });

                                
                            

Conclusion

lazy loading is a crucial optimization technique in Angular that enhances performance by deferring the loading of modules until they are needed. By understanding the differences between asynchronous and synchronous loading, developers can make informed decisions on how to manage resources efficiently. Synchronous loading, while straightforward, can lead to longer initial load times and reduced performance, especially in large applications. On the other hand, asynchronous lazy loading improves load times and user experience by loading only the necessary components when required. Implementing lazy loading in Angular involves configuring routes and modules appropriately, leveraging tools like Angular’s preloading strategies, and can be extended to handle media content and third-party libraries effectively. Ultimately, mastering lazy loading empowers developers to create more responsive and efficient Angular applications, providing a seamless experience for users.

Let's develop your ideas into reality