Angular is, by default, a powerful and high performing front-end framework. Yet, unexpected challenges are bound to happen when you’re building mission-critical web applications, apps that are content-heavy, and complex on the architectural side.
Apps will start to become slow as they grow if you are not aware of how to develop performant Angular apps and this will directly affect the end-users’ experience. A decline in traffic or no traffic, decrease in engagement, high bounce rate are some of the factors that could give you a quick idea if your application is struggling with performance.
How to solve this problem?
These problems can certainly be rectified using Angular optimization techniques. You need to analyze your application and figure out the areas which are performing slow. Also, you need to ensure that your app is adhering to the principles of clean coding architecture. There are many ways you can optimize your Angular application but there are some very important techniques that can improve your application with a great impact.
Optimization Techniques
1: OnPush change detection strategy
Change detection is one of the most common features in Angular frameworks. This is the ability to detect when the user’s data have changed or altered then, update the DOM to reflect the changes.
Default Change Detection: Angular detects the changes in the application, within the tree of components. It starts by checking the root component, then its children, then its grand-children, until all components are checked. Then all the necessary DOM updates are applied in one batch.
But this is not a very good idea to check every component on every change. And that’s often not really necessary. Whenever a change occurs, Angular starts checking all components again, starting from the root component and descending in each child component.
In the case of Reference type i.e. objects, whenever some change occurs, Angular will check each property of the object to see if there was some change and update the DOM accordingly. This can be a performance affecting action. Suppose we have large objects used in a component and its child components, Angular will check each and every property of all the objects used within the parent and all its child components whenever a change occurs.
OnPush Change Detection: With the OnPush change detection strategy, we can tell Angular not to check each component, every time the change detection runs.
With OnPush strategy, Angular will check the reference of the objects i.e. reference types and if the reference is the same, no deep comparison is performed.
For example, if a component is populating a large object and passing it along to the child components. Now if the child component is using the OnPush strategy, the change detection will check if the reference of the object passed to the chid component is changed, if yes only then it will compare the object properties.
This technique can save a lot of time and improves performance for the objects where we don’t need to check for changes every time some change occurs.
This also introduces the concept of immutability. With OnPush strategy, we don’t mutate the objects directly. Instead we create a new object (which will of course have a new reference) in order for OnPush strategy to trigger change detection.
@Component({
selector: ‘child-component',
changeDetection: ChangeDetectionStrategy.OnPush,
template: `...`
})
2: Detach Change Detection/NgZone
Every component in an Angular project tree has a change detector which creates a change detection tree. Whenever some change occurs, these change detectors tell Angular about the change which further updates the DOM accordingly.
We can inject the change detector instance into the components to either detach the component from the change detection tree or attach it to the change detection tree. So, when Angular runs change detection on the component tree, the component with its sub-tree will be skipped.
This is done by the use of the ChangeDetectorRef class. Using this change detection reference instance we can detach the change detection from any component where it is not necessary to check for changes with each change detection cycle. We can attach the change detection back anytime we want using the ChangeDetectorRef instance.
Behind the scene, Angular uses the browser APIs to make the change detection work. This is done by the Angular zone. This zone is created by Zone.js which listens on async events and tells them to Angular. We can also optimize the performance by running our code outside the Angular zone.
class TestComponent {
constructor(private changeDetectorRef: ChangeDetectorRef) {
changeDetectorRef.detach()
}
clickHandler() {
chnageDetectorRef.detectChnages()
}
}
3: Using Pure Pipes
In Angular Pipes are used to format data. Using a pipe is also more performant. There are two types of pipes, pure and impure. By default, a pipe is “pure”.
In JavaScript, a function is called “pure” if it has no side effect, and whose result only depends on its input. A pure pipe is pretty much the same: the result of its transform method only depends on arguments. Knowing that Angular applies a nice optimization: the ” transform” method is only called if the reference of the value it transforms is changed or if one of the other arguments changes. It means that whereas a method of a component is called on every change detection, a pure pipe will only be executed when needed, and only once in a template if it is used with the same input value and arguments.
4: AOT Compilation
Angular framework uses Typescript by default. In order to run the application in the browser, we need to compile (Transpire) the Typescript code and convert it into JavaScript code so that browsers can understand it.
Angular provides two compilation modes
• just-in-time (JIT)
•Ahead of time (AOT)
JIT compiles your app at runtime, and ahead-of-time (AOT) compilation occurs at build time. By default, the development compilation uses the JIT compilation. With JIT compilation, the compiler is also part of the bundle, and code is compiled at runtime. This can increase the rendering time of the components.
AOT anticipates compilation at build time, produces only the compiled templates, and removes the Angular compiler from the deployment bundle, which reduces your app payload by around 1MB (roughly the size of the Angular compiler) and rendering time is increased significantly.
5: Lazy Loading
When an Angular application is compiled, behind the scene Angular uses Webpack to create bundles that are sent over to the client. With large enterprise applications, the size of the application increases significantly with time and hence the size of the bundles also increases.
Once the main bundle starts to increase the performance goes down exponentially because every kB extra on the main bundle is contributing to slower:
Download
Parsing
JS execution
This is a performance impacting issue and can be solved by using Lazy Loading. With lazy loading, we can split our application to feature modules and load them on-demand. The main benefit is that initially, we can only load what the user expects to see at the start screen. The rest of the modules are only loaded when the user navigates to their routes. This improves application load time with a great deal.
export const routes = [
…homeRoutes,
{
path: “users”,
loadChildren: “./users/users.module#UserModule”,
}
];
Care must be taken to avoid adding any sort of reference to the feature module anywhere in the main bundle. Otherwise, it will create a compile-time dependency, and the compiler will include the feature module in the main bundle instead of lazy-loading it. That’s why we pass a string as the value of loadChildren, instead of a module reference.
6: Trackby
Angular user *ngFor structural directive to loop over data and manipulate the DOM by adding and removing DOM elements. However, if not used well, it may damage your app’s performance.
Let's say we have a large object containing User data and we need to iterate over this object and display the data on UI. We will use the *ngFor directive to iterate over the object and display the data. Now we have a scenario where we are adding records in the object asynchronously. With each addition on the object Angular change detection runs and see if the object has some new value, it will destroy the previous DOM elements and recreates the DOM for each item again. If the size of the objects even reaches over hundreds and thousands, it will re-render the whole DOM again, just for one newly added item.
This is a huge impact on performance as DOM rendering is an expensive operation. What if our *ngFor directive checks the object and only render the newly added item?
This can be done using Trackby.
7: Web Workers
It is said humans are very impatient wait for an app to load or run some computations, we will leave a site if it takes ~3s to run/load. JavaScript is single-threaded, which when running in the browser context, is called the DOM thread. During the loading of a new script in the browser, a DOM thread is created where the JS engine loads, parses and executes the JS files in the page. If our application has to perform some heavy tasks at startup i.e. calculate and render some graphs, the load time of the application will increase, waiting for the thread to complete its task.
Web apps would run better if heavy computations could be performed in the background.
The solution to this is Web Workers. The Web Worker will create a new thread called the Worker Thread that will run a JS script parallel to the DOM thread. The JS script run by the Worker thread would not have a reference to the DOM because it is running in a different environment where no DOM APIs exist.
Letting the Web Worker perform the heavy computation and loading the app using the main DOM thread can increase the load time of the application and user experience as well.
8: Preloading Modules
We have seen that with lazyloading, we load our featured modules on demand. There may be cases where we don’t want to load a feature module initially but we know that it is a popular module and it will be required soon. Once we’ve pulled down the initial bundle and loaded our application, there’s no reason to wait for a user to navigate to that popular feature before starting to load it. So its better to start loading it in the background. This is where preloading comes into play.
Preloading works with lazy loading. It’s a way to tell Angular when to start loading your feature modules. Angular comes with two default preloading strategies:
•Preload everything (PreloadAllModules)
• Don’t preload anything (NoPreloading)
You can use one of the two default preloading strategies mentioned above, or you can write your own custom preloading strategy.
You can specify which preloading strategy you want to use by passing an option into the routing configuration. Preloading can have a positive performance impact.
9: Resolve Guards
Let's say that we have to load another component from the currently loaded component which will display a list of users. In case if there is some error getting the users list from the server, we need to navigate back to the previous component with an error. Now if the server responds with an error, here are the performance impacting actions happening around.
•Currently loaded component is destroyed
•New component is loaded
•New component sending HTTP call which results in an error
•New component is destroyed
•Previous component is loaded again and displays the error
All this round trip is causing DOM rendering and destroying which is an expensive operation. This can be minimized using Resolve Guard.
We can send the required HTTP call within the Resolve Guard, which will load the next component only if the HTTP call returns success. In case of error, it will not load the next component and displays an error on an already loaded component. This can save a lot of DOM rendering and destroying time and increases performance.
10: Unsubscribe from Observables
Observables have the subscribe method which we call with a callback function to get the values emitted. Now, if we subscribe to a stream the stream will be left open and the callback will be called when values are emitted into it anywhere in the app until they are closed by calling the unsubscribe method.
If a subscription is not closed the function callback attached to it will be continuously called, this poses a huge memory leak and performance issue.
It is always a good practice to unsubscribe from observables using the OnDestroy lifecycle hook so when we leave the component, all the used observables are unsubscribed.