Forem

Atlantis
Atlantis

Posted on

Angular Performance Optimization: techniques and strategies

[Article by Soumaya Erradi]

In modern web development, high performance is crucial for delivering a seamless user experience. Angular, with its comprehensive framework, provides numerous ways to optimize performance. However, optimizing an Angular app for production requires a deeper understanding of the framework's internals and the proper use of advanced techniques. This article will cover the most effective strategies to boost performance in Angular applications, complete with advanced code examples.

1. Change Detection Strategies

Default Change Detection

Angular’s default change detection mechanism re-evaluates the entire component tree when any change is detected. This can be resource-intensive, especially in large applications with numerous components.

To understand the problem, let’s consider the following example:

@Component({
  selector: 'app-parent',
  template: `
    <app-child [data]="parentData"></app-child>
  `
})
export class ParentComponent {
  parentData = { name: 'Soumaya', age: 33 };

  updateData() {
    this.parentData.age += 1;
  }
}
Enter fullscreen mode Exit fullscreen mode

In the default strategy, Angular runs change detection on app-child every time any change happens in app-parent, regardless of whether the data passed to app-child has changed. This can lead to performance degradation.

Optimizing with OnPush

By using the OnPush change detection strategy, you can limit these checks. OnPush only runs change detection if the component’s inputs change by reference.

@Component({
  selector: 'app-child',
  templateUrl: './child.component.html',
  changeDetection: ChangeDetectionStrategy.OnPush
})
export class ChildComponent {
  @Input() data: any;
}
Enter fullscreen mode Exit fullscreen mode

However, OnPush comes with a caveat. When using complex objects or arrays as inputs, Angular doesn’t detect changes if you mutate the object without changing its reference. For example, this won't trigger change detection:

updateData() {
  this.parentData.age += 1; // Change detection won't run
}
Enter fullscreen mode Exit fullscreen mode

Instead, you should return a new object:

updateData() {
  this.parentData = { ...this.parentData, age: this.parentData.age + 1 }; // Change detection triggers
}
Enter fullscreen mode Exit fullscreen mode

Advanced Use: Manual Change Detection

For advanced cases where neither the default nor OnPush strategies are suitable, you can use manual change detection control with ChangeDetectorRef.

export class ChildComponent {
  constructor(private cdr: ChangeDetectorRef) {}

  manualCheck() {
    this.cdr.detectChanges(); // Trigger change detection manually
  }
}
Enter fullscreen mode Exit fullscreen mode

This approach is useful when you have more granular control over when change detection should run, for example, in high-performance apps like gaming or real-time financial dashboards.

2. Angular Signals

Angular Signals offer a new paradigm for managing reactivity in Angular applications. Signals allow you to manually control how and when certain parts of your app should update, leading to better performance and more predictable UI updates.

Implementing Signals

With Signals, you can avoid change detection entirely in some cases by binding the state directly to the UI.

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

const counter = signal(0);

@Component({
  selector: 'app-counter',
  template: `
    <button (click)="increment()">Increment</button>
    <p>{{ counter() }}</p>
  `
})
export class CounterComponent {
  increment() {
    counter.set(counter() + 1);
  }
}
Enter fullscreen mode Exit fullscreen mode

In this example, the counter() function updates only when the value changes, bypassing the need for traditional Angular change detection altogether. This leads to significant performance improvements in cases where you need ultra-responsive UIs.

Advanced Signals Use: Deriving State

You can also create derived signals that depend on other signals, allowing for more complex state management while maintaining high performance.

const firstName = signal('Soumaya');
const lastName = signal('Erradi');

const fullName = signal(() => `${firstName()} ${lastName()}`);

@Component({
  selector: 'app-name-display',
  template: `<p>{{ fullName() }}</p>`
})
export class NameDisplayComponent {
  updateFirstName(newName: string) {
    firstName.set(newName);
  }
}
Enter fullscreen mode Exit fullscreen mode

Here, fullName is derived from firstName and lastName, but it only recalculates when one of those values changes, avoiding unnecessary recalculations.

3. Lazy Loading Standalone Components

Lazy loading is one of the most effective ways to reduce the initial load time of an Angular app by deferring the loading of certain components or modules until they are needed.

Lazy Loading Standalone Components with Dynamic Imports

While lazy loading is common with modules, Angular 14 introduced the ability to lazy load standalone components using dynamic imports.

const routes: Routes = [
  {
    path: 'lazy',
    loadComponent: () => import('./lazy/lazy.component').then(m => m.LazyComponent)
  }
];
Enter fullscreen mode Exit fullscreen mode

This approach loads the LazyComponent only when the user navigates to the /lazy route. However, lazy loading can be further optimized by combining it with preloading strategies, ensuring that key components are loaded in the background when network bandwidth is available.

Preloading Strategy for Critical Components

In cases where you want to preload essential components without blocking the main thread, you can use Angular’s built-in preloading strategy:

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

This strategy ensures that critical components are loaded asynchronously, without affecting the user experience, thus optimizing performance for subsequent page loads.

4. Minimizing HTTP Requests

Reducing the number of HTTP requests is essential for optimizing network performance. Angular provides several built-in tools to help with caching, batching, and reducing redundant API calls.

Caching Strategy with Interceptors

One way to handle caching is by using an HTTP interceptor. This allows you to cache the results of HTTP requests and serve cached data when available, reducing the number of actual network calls.

@Injectable()
export class CacheInterceptor implements HttpInterceptor {
  private cache = new Map<string, any>();

  intercept(req: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {
    if (req.method !== 'GET') {
      return next.handle(req); // Only cache GET requests
    }

    const cachedResponse = this.cache.get(req.url);
    if (cachedResponse) {
      return of(cachedResponse);
    }

    return next.handle(req).pipe(
      tap(event => {
        if (event instanceof HttpResponse) {
          this.cache.set(req.url, event);
        }
      })
    );
  }
}
Enter fullscreen mode Exit fullscreen mode

This interceptor caches GET requests, serving them from memory for subsequent calls, drastically reducing HTTP traffic and improving app performance.

Debouncing and Throttling API Requests

In some cases, like search bars or infinite scrolling, you may want to limit the frequency of HTTP requests. You can use debounceTime or throttleTime from rxjs to accomplish this.

import { debounceTime, switchMap } from 'rxjs/operators';
import { Subject } from 'rxjs';

searchTerm$ = new Subject<string>();

this.searchTerm$.pipe(
  debounceTime(300), // Wait for 300ms of inactivity
  switchMap(term => this.http.get(`/api/search?query=${term}`))
).subscribe(results => {
  this.searchResults = results;
});
Enter fullscreen mode Exit fullscreen mode

This implementation ensures that the search request is only triggered after the user stops typing for 300ms, reducing unnecessary API calls and improving network efficiency.

5. Server-Side Rendering (SSR)

Server-Side Rendering (SSR) is built directly into the framework, making it easier to boost performance and improve SEO by rendering pages on the server. This approach results in faster initial page loads, especially for content-heavy applications, and ensures that your app is more accessible to search engines.

Setting Up SSR

Here's how you can configure it:

  1. Generate a new Angular application with SSR enabled:
   ng new angular-ssr-app --standalone --server
Enter fullscreen mode Exit fullscreen mode

This command sets up a new Angular application with SSR pre-configured and ready to go.

  1. Build and Serve the SSR Application:

After your app is set up, build it for SSR using:

   ng build --ssr
Enter fullscreen mode Exit fullscreen mode

To serve the SSR version locally:

   npm run dev:ssr
Enter fullscreen mode Exit fullscreen mode

This command runs your Angular app with server-side rendering, ensuring the server sends pre-rendered HTML to the browser for faster first paint and improved SEO.

Optimizing SSR for Performance

SSR can be further enhanced by applying advanced techniques such as route preloading, critical CSS inlining, and deferring non-essential scripts to improve the perceived performance.

  • Preload Critical Routes: Ensure that routes essential for user interaction are preloaded on the server, allowing users to interact with key sections of your app immediately.
  • Critical CSS Inlining: By inlining the CSS required for above-the-fold content, you reduce render-blocking resources, improving the perceived load time. This can be done as part of the SSR rendering process to include only the styles needed for the initial page.

Example: Preloading Key Routes and Lazy Loading Non-Essential Parts

const routes: Routes = [
  {
    path: 'home',
    component: HomeComponent,
    data: { preload: true }
  },
  {
    path: 'lazy',
    loadComponent: () => import('./lazy/lazy.component').then(m => m.LazyComponent)
  }
];
Enter fullscreen mode Exit fullscreen mode

By preloading critical routes like the home page and lazy loading non-essential components, you can strike a balance between fast initial load times and optimized resource usage.

Advanced Server Caching for SSR

To optimize performance further, implement server-side caching for frequently requested pages. This allows your server to reuse pre-rendered HTML instead of regenerating it for every request, drastically reducing response times for users.

6. Code Optimization Techniques: AOT, Lazy evaluation and more

AOT Compilation

Ahead-of-Time (AOT) compilation is an Angular feature that pre-compiles templates during the build phase. This reduces the amount of work the browser needs to do at runtime, improving performance.

ng build --aot
Enter fullscreen mode Exit fullscreen mode

AOT ensures that your application ships optimized and smaller JavaScript bundles to the client, making it one of the easiest ways to speed up your Angular app.

Advanced Code Splitting with Dynamic Imports

In some cases, you may want to split specific parts of your application into separate bundles to load them dynamically, reducing the size of the initial payload.

import('./heavy-module/heavy-module.component').then(m => m.HeavyModule);
Enter fullscreen mode Exit fullscreen mode

This ensures that only when the component is needed does it get downloaded and executed.

Avoiding Expensive Template Operations

Angular templates are often where performance issues arise. Avoid heavy logic in templates and instead delegate complex calculations to the component class or use ngOnInit.

export class AppComponent {
  result: number;

  ngOnInit() {
    this.result = this.calculateComplexOperation();
  }

  calculateComplexOperation() {
    // Expensive operation here
    return ...;
  }
}
Enter fullscreen mode Exit fullscreen mode

By pre-calculating data in the component class, you avoid recalculating it on every change detection cycle, significantly improving performance.

Conclusion

Angular provides a wealth of tools to optimize performance, but advanced techniques like Signals, manual change detection, SSR, and caching with interceptors can take your optimizations to the next level. Combining these strategies allows you to create fast, scalable, and highly performant Angular applications that provide a better user experience. Regularly monitor performance using tools like Chrome DevTools and Lighthouse to identify bottlenecks and ensure your optimizations are effective.

Top comments (0)