4 Best Practices and Techniques to Optimize Your Angular Application

4 Best Practices and Techniques to Optimize Your Angular Application

10 years after its release, Angular has become a dominant JavaScript framework in front-end development. Many companies and developers continue to use Angular to develop complex web and mobile applications thanks to its distinct features and robust characteristics.

By default, Angular provides peak performance. But when you're building a high-level enterprise system or as your application architecture grows, you may start to notice a lag in the application. When this happens, it has several consequences for your app:

  • Poor user experience
  • Increased site bounce rate: Users may switch to a similar platform when an app takes too long to load
  • Reduced data traffic on your platform

So, what causes performance degradation in Angular applications? The reasons vary, but I've found the following common culprits:

  • Developers' failure to follow best development practices
  • Failure to use optimization techniques

With this in mind, let's cover the necessary techniques and practices you need to optimize your Angular application. But first, let's cover why performance optimization is critical in your Angular application.

Performance Optimization: Why Does It Matter?

App performance is the most significant reason users will switch to your platform. Remember, the better the user experience and site speed, the more you stand to gain widespread customer recognition.

Recent research by Google shows that 53% of mobile users will exit a webpage if it doesn't load in 3 seconds. This isn't good for your business, especially in a highly competitive market.

As your application scales, pay close attention to load time and performance optimization. If you're a large development team, always discuss this with the developers and product managers. More importantly, listen to your users about your app's user experience.

Implement a Change Detection Strategy

A vital feature of the Angular framework is change detection.

Change detection allows Angular to update the DOM when there's a change in user data.

In the days of AngularJS (1.x), this feature was known as the digest cycle. When Angular switched to 2.x, the process remained the same, albeit differently, but abided by the same philosophy.

With this in mind, let's cover the two core change detection strategies:

  • Default
  • OnPush

Default Change Detection

Angular's hierarchical component architecture allows it to detect new changes a user makes within its tree of components.

How does this happen?

Angular starts by checking changes from the root component, then the child, followed by the grand-child components, and so on. These changes are implemented in the DOM in one installment.

If you're wondering whether this is a good practice, it's not. Checking all the components from the root to the descendants is bound to affect the performance of an application.

Imagine a scenario where you have a large object used in the root component and the child components. If a change occurs, Angular checks each property for every object referenced in that component and the descendant child components. When this happens continuously, it negatively alters the application's performance.

To curb this, Angular uses the OnPush detection strategy.

OnPush Change Detection

With this strategy, we can tell Angular not to check each component every time a change occurs. In a way, this makes our components smarter.

OnPush detection strategy revokes the current component change detection only when a change occurs in the incoming ' @input' binding value. To put it another way, we can say this strategy runs change detection for descendant components only. With unnecessary change detection iterations out of the way, it significantly improves the performance of your application.

@Component({
     selector: 'child-component',
     changeDetection: ChangeDetectionStrategy.OnPush,
     template: `...`
})

With OnPush change detection, you also introduce the principle of immutability. This means we don't mutate the objects directly. Instead, the option is to create a new object (with a unique reference) that triggers change detection with the OnPush strategy.

Lazy Loading

When you're building an enterprise-level application, you need to divide the core functionalities into a set of chunks, and these chunks are called modules or sub-modules. Every module offers a core feature—such features are pluggable and easy to maintain.

However, trouble comes in when you have a large application consisting of 10+ modules and a large code volume. As the bundle size grows with application scalability, loading large JS files can affect boot time, which is bad for your application.

Let's look at the code below:

const routes: Routes = [
 {path: 'dashboard', component: 'DashboardModule'},
 {path: 'profile', component: 'ProfileModule'},
 {path: 'cart', component: 'CartModule'},
 {path: '**', redirectTo: 'dashboard'}
]

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

Can you tell me what's wrong with the code above? Our application loads all modules and components on the initial page load at boot time, even though we only have a single module operating on the page screen. This means Dashboard, Profile, and Cart modules will load on the actual page load.

Is there a way to make this system better?

Yes. Through lazy loading. We can tell the application to only load functional modules on the specific screen. This feature is achievable through the Router API. With this feature, you only load modules on demand and lazily based on a particular route. This way, you improve the application's boot-up speed, and this refines the performance optimization of your application.

Here's how you solve it:

const routes: Routes = [
 {path: 'dashboard', loadChildren: './profile# DashboardModule'},
 {path: 'profile', loadChildren: './profile# ProfileModule'},
 {path: 'cart', loadChildren: './profile# CartModule'},
 {path: '**', redirectTo: 'dashboard'}
]

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

Use Pure Pipes

Using pipes in Angular allows the transformation of data. In addition, pipes increase the performance of an application. In Angular, you can use pipes to transform different sets of information, such as dates, strings, and currency.

Here's an example of a pipe date: *date | shortdate* - it converts the date object to a short date format like *dd/MM/yyyy*.

In Angular, we divide pipes into two major categories:

  • Pure: Produces the same results for the same input over time.
  • Impure: Produces different results for the same input over time.

Generally, when it comes to binding evaluation, Angular evaluates the expression and afterward applies the pipe over it (if it exists). Next, Angular caches the value of that specific binding, similar to memorization. If the same event tries to re-evaluate, Angular fetches the same value from the binding level cache. Angular conducts this right-of-the-bat technique for pure pipes.

However, when building an enterprise-level application, the simple use case explained above needs more than that to execute. Solving this requires you to implement your memorization technique. Next, you want to acquire extra performance benefits. This means caching a value after every visit. Later, when the same value arrives, you return the cached value.

Here's a simple code base:

import { Pipe } from '@angular/core';
import { FilterPipe }from './filter. pipe'; 

@Pipe({
   name: 'filterPipe', 
   pure: true     
 })
 export class FilterPipe {}

Unsubscribe From Observables

Developing an Angular application requires you to be cautious to avoid minor setbacks that may contribute to memory leaks.

In most cases, memory leaks happen when an Angular application falters in disposing of resources that aren't in use after execution or during application run time. Observables are the best culprits for memory leaks. Since observables have a subscribe method which we call with a callback function, they create an open stream that remains open until it's closed with an unsubscribe method.

For instance, declaring global variables makes redundant memory leaks that do not unsubscribe observables.

The best workaround is to ensure you unsubscribe from observables using the OnDestroy lifecycle hook. This way, the application performs optimally with all the observables unsubscribed.

interface OnDestroy {
  ngOnDestroy(): void
}

Takeaway on Angular App Optimization

I hope you have gained as much as possible from this guide. We have explored the best techniques and optimization practices that will improve your application performance. Implementing the tips above will significantly improve user experience in your application and help fine-tune your Angular apps.