[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;
}
}
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;
}
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
}
Instead, you should return a new object:
updateData() {
this.parentData = { ...this.parentData, age: this.parentData.age + 1 }; // Change detection triggers
}
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
}
}
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);
}
}
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);
}
}
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)
}
];
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 {}
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);
}
})
);
}
}
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;
});
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:
- Generate a new Angular application with SSR enabled:
ng new angular-ssr-app --standalone --server
This command sets up a new Angular application with SSR pre-configured and ready to go.
- Build and Serve the SSR Application:
After your app is set up, build it for SSR using:
ng build --ssr
To serve the SSR version locally:
npm run dev:ssr
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)
}
];
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
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);
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 ...;
}
}
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)