Performance is always a concern in any product that you build for end users. It is a critical element in increasing the chances of someone using your app for the first time becoming a customer. Now, we can't really improve an app's performance until we identify potential possibilities for improvement and the methods to achieve this. In this chapter, you'll learn some methods to deploy when it comes to improving Angular applications. You'll learn how to analyze, optimize, and improve your Angular app's performance using several techniques. Here are the recipes we're going to cover in this chapter:
For the recipes in this chapter, make sure you have Git and Node.js installed on your machine. You also need to have the @angular/cli package installed, which you can do with npm install -g @angular/cli from your terminal. The code for this chapter can be found at the following link: https://github.com/PacktPublishing/Angular-Cookbook/tree/master/chapter12.
In today's world of modern web applications, performance is one of the key factors for a great user experience (UX) and, ultimately, conversions for a business. In this recipe, being the first recipe of this chapter, we're going to discuss the fundamental or the most basic optimization you can do with your components wherever it seems appropriate, and that is by using the OnPush change-detection strategy.
The project we are going to work with resides in Chapter12/start_here/using-onpush-change-detection, inside the cloned repositor:
Now that we have the project served on the browser, let's see the steps of the recipe in the next section.
The app we're working with has some performance issues, particularly with the UserCardComponent class. This is because it is using the idUsingFactorial() method to generate a unique ID to show on the card. We're going to experience and understand the performance issue this causes. We will try to fix the issue using the OnPush change-detection strategy. Let's get started:
Let's add some logic to the code. We'll check how many times Angular calls the idUsingFactorial() method when the page loads.
...
@Component({...})
export class UserCardComponent implements OnInit {
...
constructor(private router: Router) {}
ngOnInit(): void {
if (!window['appLogs']) {
window['appLogs'] = {};
}
if (!window['appLogs'][this.user.email]) {
window['appLogs'][this.user.email] = 0;
}
}
...
idUsingFactorial(num, length = 1) {
window['appLogs'][this.user.email]++;
if (num === 1) {...} else {...}
}
}
Notice the count when calling the idUsingFactorial() method for [email protected]. It has increased from 40 to 300 now, in just a few key presses.
Let's use the OnPush change-detection strategy now. This will avoid the Angular change-detection mechanism running on each browser event, which currently causes a performance issue on each key press.
import {
ChangeDetectionStrategy,
Component,
...
} from '@angular/core';
...
@Component({
selector: 'app-user-card',
templateUrl: './user-card.component.html',
styleUrls: ['./user-card.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class UserCardComponent implements OnInit {
...
}
Notice that even after refreshing the app, and after typing the name Elfie Siegert, we now have a very low number of calls to the idUsingFactorial() method. For example, for the [email protected] email address, we only have 20 hits, instead of the initial 40 hits shown in Figure 12.2, and 300 hits, as shown in Figure 12.3, after typing.
Great! Within a single step, by using the OnPush strategy we were able to improve the overall performance of our UserCardComponent. Now you know how to use this strategy, see the next section to understand how it works.
Angular by default uses the Default change-detection strategy—or technically, it is the ChangeDetectionStrategy.Default enum from the @angular/core package. Since Angular doesn't know about every component we create, it uses the Default strategy to not encounter any surprises. But as developers, if we know that a component will not change unless one of its @Input() variables changes, we can—and we should—use the OnPush change-detection strategy for that component. Why? Because it tells Angular to not run change detection until an @Input() variable for the component changes. This strategy is an absolute winner for presentational components (sometimes called dumb components), which are just supposed to show data using @Input() variables/attributes, and emit @Output() events on interactions. These presentational components usually do not hold any business logic such as heavy computation, using services to make HyperText Transfer Protocol (HTTP) calls, and so on. Therefore, it is easier for us to use the OnPush strategy in these components because they would only show different data when any of the @Input() attributes from the parent component change.
Since we are now using the OnPush strategy on our UserCardComponent, it only triggers change detection when we replace the entire array upon searching. This happens after the 300ms debounce (line 28 in the users.component.ts file), so we only do it when the user stops typing. So, essentially, before the optimization, the default change detection was triggering on each keypress being a browser event, and now, it doesn't.
Important note
As you now know that the OnPush strategy only triggers the Angular change-detection mechanism when one or more of the @Input() bindings changes, this means that if we change a property within the component (UserCardComponent), it will not be reflected in the view because the change-detection mechanism won't run in this case, since that property isn't an @Input() binding. You would have to mark the component as dirty so that Angular could check the component and run change detection. You'll do this using the ChangeDetectorRef service—specifically, with the .markForCheck() method.
In the previous recipe, we learned how to use the OnPush strategy in our components to avoid Angular change detection running unless one of the @Input() bindings has changed. There is, however, another way to tell Angular to not run change detection at all, in any instance. This is handy when you want full control on when to run change detection. In this recipe, you'll learn how to completely detach the change detector from an Angular component to gain performance improvements.
The project for this recipe resides in Chapter12/start_here/detaching-change-detecto:
Now that we have the project served on the browser, let's see the steps of the recipe in the next section.
We have the same users list application but with a twist. Right now, we have the UserSearchInputComponent component that holds the search input box. This is where we type the username to search for it in the users list. On the other hand, we have the UserCardListComponent component that has a list of users. We'll first experience the performance issues, and then we'll detach the change detector smartly to gain performance improvements. Let's get starte:
The preceding screenshot shows that the idUsingFactorial() method in the UserCardComponent class for the [email protected] user has been called about 100 times, just in the steps we've performed so far.
You'll notice that the app immediately hangs, and it takes a few seconds to show the user. You'll also notice that you don't even see the letters being typed in the search box as you type them. If you've followed Step 1 and Step 2 correctly, you should see an appLogs object, as follows:
You can see in the preceding screenshot that the idUsingFactorial() method for the [email protected] user has now been called about 220 times.
import { ChangeDetectorRef, Component, OnInit} from '@angular/core';
...
@Component({...})
export class UsersComponent implements OnInit {
users: IUser[];
constructor(
private userService: UserService,
private cdRef: ChangeDetectorRef
) {}
ngOnInit() {
this.cdRef.detach();
this.searchUsers();
}
}
If you refresh the app now, you'll see… Actually, you won't see anything, and that's fine—we have more steps to follow.
...
@Component({...})
export class UsersComponent implements OnInit {
...
searchUsers(searchQuery = '') {
this.userService.searchUsers(
searchQuery).subscribe((users) => {
this.users = users;
this.cdRef.detectChanges();
});
}
...
}
You can see in the preceding screenshot that even after performing all the actions mentioned in Step 1 and Step 2, we have a very low count of the change-detection run cycle.
Awesomesauce! You've just learned how to detach the Angular change detector using the ChangeDetectorRef service. Now that you've finished the recipe, see the next section to understand how it works.
The ChangeDetectorRef service provides a bunch of important methods to control change detection completely. In the recipe, we use the .detach() method in the ngOnInit() method of the UsersComponent class to detach the Angular change-detection mechanism from this component as soon as it is created. As a result, no change detection is triggered on the UsersComponent class, nor in any of its children. This is because each Angular component has a change-detection tree in which each component is a node. When we detach a component from the change-detection tree, that component (as a tree node) is detached, and so are its child components (or nodes). By doing this, we end up with absolutely no change detection happening for the UsersComponent class. As a result, when we refresh the page nothing is rendered, even after we've got the users from the application programming interface (API) and have got them assigned to the users property inside the UsersComponent class. Since we need to show the users on the view, which requires the Angular change-detection mechanism to be triggered, we use the .detectChanges() method from the ChangeDetectorRef instance, right after we've assigned the users data to the users property. As a result, Angular runs the change-detection mechanism, and we get the user cards shown on the view.
This means that in the entire Users page (that is, on the /users route) the only time the Angular change-detection mechanism would trigger after the UsersComponent class has initiated is when we call the searchUsers() method, get the data from the API, and assign the result to the users property, thus creating a highly controlled change-detection cycle, resulting in much better performance overall.
Angular runs its change-detection mechanism on a couple of things, including—but not limited to—all browser events such as keyup, keydown, and so on. It also runs change detection on setTimeout, setInterval, and Ajax HTTP calls. If we had to avoid running change detection on any of these events, we'd have to tell Angular not to trigger change detection on them—for example, if you were using the setTimeout() method in your Angular component, it would trigger an Angular change detection each time its callback method was called. In this recipe, you'll learn how to execute code blocks outside of the ngZone service, using the runOutsideAngular() method.
The project for this recipe resides in Chapter12/start_here/run-outside-angula:
Now that we have the app running, let's see the steps of the recipe in the next section.
We have an app that shows a watch. However, the change detection right now in the app is not optimal, and we have plenty of room for improvement. We'll try to remove any unnecessary change detection using the runOutsideAngular method from ngZone. Let's get starte:.
...
@Component({...})
export class WatchBoxComponent implements OnInit {
...
ngOnInit(): void {
this.intervalTimer = setInterval(() => {
this.timer();
}, 1);
setTimeout(() => {
clearInterval(this.intervalTimer);
}, 4000);
}
...
}
...
@Component({...})
export class WatchComponent implements OnInit {
...
ngOnInit(): void {
this.intervalTimer = setInterval(() => {
this.animate();
}, 30);
setTimeout(() => {
clearInterval(this.intervalTimer);
}, 4000);
}
...
}
Refresh the app and wait for the animation to stop. Have a look at the appLogs object in the Chrome DevTools. You should see that change detection stops for the watch key, as follows:
...
@Component({...})
export class WatchBoxComponent implements OnInit {
...
ngOnInit(): void {
// this.intervalTimer = setInterval(() => {
// this.timer();
// }, 1);
// setTimeout(() => {
// clearInterval(this.intervalTimer);
// }, 4000);
}
}
Since we've now stopped the clock, the values for appLogs for the watch key are now only based on the animation for these 4 seconds. You should now see a value between 250 and 260 for the watch key.
import {
...
ViewChild,
NgZone,
} from '@angular/core';
@Component({...})
export class WatchComponent implements OnInit {
...
constructor(private zone: NgZone) {
...
}
ngOnInit(): void {
this.zone.runOutsideAngular(() => {
this.intervalTimer = setInterval(() => {
this.animate();
}, 30);
setTimeout(() => {
clearInterval(this.intervalTimer);
}, 2500);
});
}
...
}
Refresh the app and wait for about 5 seconds. If you check the appLogs object now, you should see a decrease in the overall number of change-detection runs for each of the properties, as follows:
Yayy! Notice that the value for the watch key in the appLogs object has decreased from about 250 to 4 now. This means that our animation now doesn't contribute to change detection at all.
...
@Component({...})
export class WatchComponent implements OnInit {
...
ngOnInit(): void {
...
this.ngZone.runOutsideAngular(() => {
this.intervalTimer = setInterval(() => {
this.animate();
}, 30);
setTimeout(() => { // ← Remove this block
clearInterval(this.intervalTimer);
}, 4000);
});
}
...
}
import { Component, OnInit } from '@angular/core';
@Component({
selector: 'app-watch-box',
templateUrl: './watch-box.component.html',
styleUrls: ['./watch-box.component.scss'],
})
export class WatchBoxComponent implements OnInit {
name = '';
time = {
hours: 0,
minutes: 0,
seconds: 0,
milliseconds: 0,
};
intervalTimer;
constructor() {}
ngOnInit(): void {
this.intervalTimer = setInterval(() => {
this.timer();
}, 1);
setTimeout(() => { // ← Remove this
clearInterval(this.intervalTimer);
}, 4000);
}
...
}
Refresh the app and check the value of the appLogs object after a few seconds, multiple times. You should see something like this:
Looking at the preceding screenshot, you'd be like: "Ahsan! What is this? We still have a huge number for the change-detection runs for the watch key. How is this performant exactly?" Glad you asked. I will tell you the why in the How it works… section.
ng serve --prod
Boom! If you look at the preceding screenshot, you should see that the change-detection run count for the watch key is always just one cycle more than the milliseconds key. This means that the WatchComponent class is almost only re-rendered whenever we have the value of the @Input() milliseconds binding updated.
Now that you've finished the recipe, see the next section to understand how it all works.
In this recipe, we begin by looking at the appLogs object, which contains some key-value pairs. The value for each key-value pair represents the number of times Angular ran change detection for a particular component. The hours, milliseconds, minutes, and seconds keys represent the WatchTimeComponent instance for each of the values shown on the clock. The watch key represents the WatchComponent instance.
At the beginning of the recipe, we see that the value for the watch key is more than twice the value of the milliseconds key. Why do we care about the milliseconds key at all? Because the @Input() attribute binding milliseconds changes most frequently in our application—that is, it changes every 1 millisecond (ms). The second most frequently changed values are the xCoordinate and yCoordinates properties within the WatchComponent class, which change every 30 ms. The xCoordinate and yCoordinate values aren't bound directly to the template (the HyperText Markup Language (HTML)) because they change the Cascading Style Sheets (CSS) variables of the stopWatch view child. This happens inside the animate() method, as follows:
el.style.setProperty('--x', `${this.xCoordinate}px`);
el.style.setProperty('--y', `${this.yCoordinate}px`);
Thus, changing these values shouldn't actually trigger change detection at all. We begin by limiting the clock window, using the clearInterval() method in the WatchBoxComponent class so that the clock stops within 4 seconds and we can evaluate the numbers. In Figure 12.11, we see that even after the clock stops, the change-detection mechanism keeps triggering for the WatchComponent class. This increases the count for the watch key in the appLogs object as time passes. We then stop the animation by using clearInterval() in the WatchComponent class. This stops the animation after 4 seconds as well. In Figure 12.12, we see that the count for the watch key stops increasing after the animation stops.
We then try to see the count of change detection only based on the animation. In Step 6, we stop the clock. Therefore, we only get a count based on the animation in the appLogs object for the watch key, which is a value between 250 and 260.
We then introduce the magic runOutsideAngular() method into our code. This method is part of the NgZone service. The NgZone service is packaged with the @angular/core package. The runOutsideAngular() method accepts a method as a parameter. This method is executed outside the Angular zone. This means that the setTimeout() and setInterval() methods used inside the runOutsideAngular() method do not trigger the Angular change-detection cycle. You can see in Figure 12.13 that the count drops to 4 after using the runOutsideAngular() method.
We then remove the clearInterval() usage from both the WatchBoxComponent and the WatchComponent classes—that is, to run the clock and the animation again, as we did in the beginning. In Figure 12.14, we see that the count for the watch key is almost twice the value of the milliseconds key. Now, why is that double exactly? This is because in development mode, Angular runs the change-detection mechanism twice. Therefore, in Step 9 and Step 10, we run the application in production mode, and in Figure 12.15, we see that the value for the watch key is just one greater than the value for the milliseconds key, which means that the animation does not trigger any change detection for our application any more. Brilliant, isn't it? If you found this recipe useful, do let me know on my socials.
Now that you understand how it works, see the next section for further reading.
Lists are an essential part of most of the apps we build today. If you're building an Angular app, there's a great chance you will use the *ngFor directive at some point. We know that *ngFor allows us to loop over arrays or objects generating HTML for each item. However, for large lists, using it may cause performance issues, especially when the source for *ngFor is changed completely. In this recipe, we'll learn how we can improve the performance of lists using the *ngFor directive with the trackBy function. Let's get started.
The project for this recipe resides in Chapter12/start_here/using-ngfor-trackb:
Now that we have the app running, let's see the steps of the recipe in the next section.
We have an app that has a list of 1,000 users displayed on the view. Since we're not using a virtual scroll and a standard *ngFor list, we do face some performance issues at the moment. Notice that when you refresh the app, even after the loader is hidden, you see a blank white box for about 2-3 seconds before the list appears. Let's start the recipe to reproduce the performance issues and to fix them.
...
@Component({...})
export class TheAmazingListComponent implements OnInit {
...
ngOnInit(): void {}
trackByFn(_, user: AppUserCard) {
return user.email;
}
}
<h4 class="heading">Our trusted customers</h4>
<div class="list list-group">
<app-list-item
*ngFor="let item of listItems; trackBy: trackByFn"
[item]="item"
(itemClicked)="itemClicked.emit(item)"
(itemDeleted)="itemDeleted.emit(item)"
>
</app-list-item>
</div>
Great!! You now know how to use the trackBy function with the *ngFor directive to optimize the performance of lists in Angular. To understand all the magic behind the recipe, see the next section.
The *ngFor directive by default assumes that the object itself is its unique identity, which means that if you just change a property in an object used in the *ngFor directive, it won't re-render the template for that object. However, if you provide a new object in its place (different reference in memory), the content for the particular item will re-render. This is what we actually do in this recipe to reproduce the performance-issue content. In the data.service.ts file, we have the following code for the updateUser() method:
updateUser(updatedUser: AppUserCard) {
this.users = this.users.map((user) => {
if (user.email === updatedUser.email) {
return {
...updatedUser,
};
}
// this tells angular that every object has now a different reference
return { ...user };
});
}
Notice that we use the object spread operator ( { … } ) to return a new object for each item in the array. This tells the *ngFor directive to re-render the UI for each item in the listItems array in the TheAmazingListComponent class. Suppose you send a query to the server to find or filter users. The server could return a response that has 100 users. Out of those 100, about 90 were already rendered on the view, and only 10 are different. Angular, however, would re-render the UI for all the list items because of the following potential reasons (but not limited to these):
Now, we want to avoid using the object reference as the unique identifier for each list item. For our use case, we know that each user's email is unique, therefore we use the trackBy function to tell Angular to use the user's email as the unique identifier. Now, even if we return a new object for each user after a user update from the updateUser() method (as previously shown), Angular doesn't re-render all the list items. This is because the new objects (users) have the same email and Angular uses it to track them. Pretty cool, right?
Now that you've learned how the recipe works, see the next section to view a link for further reading.
In Angular, we have a particular way of writing components. Since Angular is heavily opinionated, we already have a lot of guidelines from the community and the Angular team on what to consider when writing components—for example, making HTTP calls directly from a component is considered a not-so-good practice. Similarly, if we have heavy computation in a component, this is also not considered a good practice. And when the view depends upon a transformed version of the data using a computation constantly, it makes sense to use Angular pipes. In this recipe, you'll learn how to use Angular pure pipes to avoid heavy computation within components.
The project we are going to work with resides in Chapter12/start_here/using-pure-pipes, inside the cloned repositor:
Now that we have the project served on the browser, let's see the steps of the recipe in the next section.
The app we're working with has some performance issues, particularly with the UserCardComponent class because it uses the idUsingFactorial() method to generate a unique ID to show on the card. You'll notice that if you try typing 'irin' in the search box, the app hangs for a while. We're not able to see the letters being typed instantly in the search box, and it takes a while before the results show. We will fix the issues by moving the computation in the idUsingFactorial() method to an Angular (pure) pipe. Let's get starte:
ng g pipe core/pipes/unique-id
...
@Pipe({...})
export class UniqueIdPipe implements PipeTransform {
characters = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdef ghijklmnopqrstuvwxyz0123456789';
createUniqueId(length) {
var result = '';
const charactersLength = this.characters.length;
for (let i = 0; i < length; i++) {
result += this.characters.charAt(
Math.floor(Math.random() * charactersLength)
);
}
return result;
}
...
transform(index: unknown, ...args: unknown[]): unknown {
return null;
}
}
import { Pipe, PipeTransform } from '@angular/core';
@Pipe({
name: 'uniqueId',
})
export class UniqueIdPipe implements PipeTransform {
...
idUsingFactorial(num, length = 1) {
if (num === 1) {
return this.createUniqueId(length);
} else {
const fact = length * (num - 1);
return this.idUsingFactorial(num - 1, fact);
}
}
transform(index: number): string {
return this.idUsingFactorial(index);
}
}
<div class="user-card">
<div class="card" *ngIf="user" (click)="cardClicked()">
<img [src]="user.picture.large" class="card-img-top" alt="..." />
<div class="card-body">
<h5 class="card-title">{{ user.name.first }} {{ user.name.last }}</h5>
<p class="card-text">{{ user.email }}</p>
<p class="card-text unique-id" title="{{ index | uniqueId }}">
{{ index | uniqueId }}
</p>
<a href="tel: {{ user.phone }}" class="btn btn-primary">{{
user.phone
}}</a>
</div>
</div>
</div>
Boom! Now that you know how to optimize performance by moving heavy computation to pure Angular pipes, see the next section to understand how this works.
As we know, Angular by default runs change detection on each browser event triggered in the app, and since we're using an idUsingFactorial() method in the component template (UI), this function runs each time Angular runs the change-detection mechanism, causing more computation and performance issues. This would also hold true if we used a getter instead of a method. Here, we use a method because each unique ID is dependent on the index and we need to pass the index in the method when calling it.
We can take a step back from the initial implementation and think what the method actually does. It takes an input, does some computation, and returns a value based on the input—a classic example of data transformation, and also an example of where you would use a pure function. Luckily, Angular pure pipes are pure functions, and they do trigger change detection unless the input changes.
In the recipe, we move the computation to a newly created Angular pipe. The pipe's transform() method receives the value to which we're applying the pipe, which is the index of each user card in the users array. The pipe then uses the idUsingFactorial() method and, ultimately, the createUniqueId() method to calculate a random unique ID. When we start typing in the search box, the values for the index do not change. This results in no change detection being triggered until we get back a new set of users as output. Therefore, there is no unnecessary computation run as we type the input into the search box, thus optimizing performance and unblocking the UI thread.
If your Angular application does a lot of computation during an action, there's a great chance that it will block the UI thread. This will cause a lag in rendering the UI because it blocks the main JavaScript thread. Web workers allow us to run heavy computation in the background thread, thus freeing the UI thread as it is not blocked. In this recipe, we're going to use an application that does a heavy computation in the UserService class. It creates a unique ID for each user card and saves it into the localStorage. However, it loops a couple of thousand times before doing so, which causes our application to hang for a while. In this recipe, we'll move the heavy computation from the components to a web worker and will also add a fallback in case web workers aren't available.
The project we are going to work with resides in Chapter12/start_here/using-web-workers, inside the cloned repositor:
Now that we have the app running, let's see the steps of the recipe in the next section.
Once you open the app, you'll notice that it takes some time before the user cards are rendered. This shows that the UI thread is blocked until we have the computation finished. The culprit is the saveUserUniqueIdsToStorage() method in the UserService class. This generates a unique ID a couple of thousands of times before saving it to the localStorage. Let's start the recipe, to improve the performance of the app. We'll start by implementing the web worke:
ng generate web-worker core/workers/idGenerator
/// <reference lib="webworker" />
import createUniqueId from '../constants/create-unique-id';
addEventListener('message', ({ data }) => {
console.log('message received IN worker', data);
const { index, email } = data;
let uniqueId;
for (let i = 0, len = (index + 1) * 100000; i < len; ++i) {
uniqueId = createUniqueId(50);
}
postMessage({ uniqueId, email });
});
let UNIQUE_ID_WORKER: Worker = null;
const getUniqueIdWorker = (): Worker => {
if (typeof Worker !== 'undefined' && UNIQUE_ID_WORKER === null) {
UNIQUE_ID_WORKER = new Worker('../workers/ id-generator.worker', {
type: 'module',
});
}
return UNIQUE_ID_WORKER;
};
export default getUniqueIdWorker;
...
import getUniqueIdWorker from '../constants/get-unique-id-worker';
@Injectable({...})
export class UserService {
...
worker: Worker = getUniqueIdWorker();
constructor(private http: HttpClient) {
this.worker.onmessage = ({ data: { uniqueId, email } }) => {
console.log('received message from worker', uniqueId, email);
const user = this.usersCache.find((user) => user. email === email);
localStorage.setItem(
`ng_user__${user.email}`,
JSON.stringify({
...user,
uniqueId,
})
);
};
}
...
}
...
@Injectable({...})
export class UserService {
...
saveUserUniqueIdsToStorage(user: IUser, index) {
let uniqueId;
const worker: Worker = getUniqueIdWorker();
if (worker !== null) {
worker.postMessage({ index, email: user.email });
} else {
// fallback
for(let i = 0, len = (index + 1) * 100000; i<len; ++i) {
uniqueId = createUniqueId(50);
}
localStorage.setItem(...);
}
}
...
}
Woohoo!!! The power of web workers! And now you know how to use web workers in an Angular app to move heavy computation to them. Since you've finished the recipe, see the next section on how this works.
As we discussed in the recipe's description, web workers allow us to run and execute code in a separate thread from the main JavaScript (or UI thread). At the beginning of the recipe, whenever we refresh the app or search for a user, it blocks the UI thread. This is until a unique ID is generated for each card. We begin the recipe by creating a web worker using the Angular command-line interface (CLI). This creates an id-generator.worker.ts file, which contains some boilerplate code to receive messages from the UI thread and to send a message back to it as a response. The CLI command also updates the angular.json file by adding a webWorkerTsConfig property. The value against the webWorkerTsConfig property is the path to the tsconfig.worker.json file, and the CLI command also creates this tsconfig.worker.json file. If you open the tsconfig.worker.json file, you should see the following code:
/* To learn more about this file see: https://angular.io/config/tsconfig. */
{
"extends": "./tsconfig.json",
"compilerOptions": {
"outDir": "./out-tsc/worker",
"lib": [
"es2018",
"webworker"
],
"types": []
},
"include": [
"src/**/*.worker.ts"
]
}
After creating a web worker file, we create another file named uniqueIdWorker.ts. This file exports the getUniqueIdWorker() method as the default export. When we call this method, it generates a new Worker instance if we don't have a worker generated already. The method uses the id-generator.worker.ts file to generate a worker. We also use the addEventListener() method inside the worker file to listen to the messages sent from the UI thread (that is, the UserService class). We receive the index of the user card and the email of the user as the data in this message. We then use a for loop to generate a unique ID (uniqueId variable), and once the loop ends, we use the postMessage() method to send the uniqueId variable and the email back to the UI thread.
Now, in the UserService class, we listen to messages from the worker. In the constructor() method, we check if web workers are available in the environment by checking the value from the getUniqueIdWorker() method, which should be a non-null value. Then, we use the worker.onmessage property to assign it a method. This is to listen to the messages from the worker. Since we already know that we get the uniqueId variable and the email from the worker, we use the email to get the appropriate user from the usersCache variable. Then, we store the user data with the uniqueId variable to the localStorage against the user's email.
Finally, we update the saveUserUniqueIdsToStorage() method to use the worker instance if it is available. Notice that we use the worker.postMessage() method to pass the index and the email of the user. Note also that we are using the previous code as a fallback for cases where we don't have web workers enabled.
In today's world, most of the population has a pretty good internet connection to use everyday applications, be it a mobile app or a web app, and it is fascinating how much data we ship to our end users as a business. The amount of JavaScript shipped to users has an ever-increasing trend now, and if you're working on a web app, you might want to use performance budgets to make sure the bundle size doesn't exceed a certain limit. With Angular apps, setting the budget sizes is a breeze. In this recipe, you're going to learn how to use the Angular CLI to set up budgets for your Angular apps.
The project for this recipe resides in Chapter12/start_here/angular-performance-budget:
Notice that the bundle size for the main.*.js file is about 260 kilobytes (KB) at the moment. Now that we have built the app, let's see the steps of the recipe in the next section.
We have an app that is really small in terms of bundle size at the moment. However, this could grow into a huge app with upcoming business requirements. For the sake of this recipe, we'll increase the bundle size deliberately and will then use performance budgets to stop the Angular CLI from building the app for production if the bundle size exceeds the budget. Let's begin the recip:
...
import * as moment from '../lib/moment';
import * as THREE from 'three';
@Component({...})
export class AppComponent {
...
constructor(private auth: AuthService, private router: Router) {
const scene = new THREE.Scene();
console.log(moment().format('MMM Do YYYY'));
}
...
}
Let's suppose our business requires us to not ship apps with main bundle sizes more than 1.0 MB. For this, we can configure our Angular app to throw an error if the threshold is met.
...
{
"budgets": [
{
"type": "initial",
"maximumWarning": "800kb",
"maximumError": "1mb"
},
{
"type": "anyComponentStyle",
"maximumWarning": "6kb",
"maximumError": "10kb"
}
]
}
...
npm install --save date-fns
import { Component } from '@angular/core';
import { Router } from '@angular/router';
import { AuthService } from './services/auth.service';
import { format } from 'date-fns';
import { Scene } from 'three';
@Component({...})
export class AppComponent {
...
constructor(private auth: AuthService, private router: Router) {
console.log(format(new Date(), 'LLL do yyyy'));
const scene = new Scene();
}
...
}
Boom!! You just learned how to use the Angular CLI to define performance budgets. These budgets can be used to throw warnings and errors based on your configuration. Note that the budgets can be modified based on changing business requirements. However, as engineers, we have to be cautious about what we set as performance budgets to not ship JavaScript over a certain limit to the end users.
In the previous recipe, we looked at configuring budgets for our Angular app, and this is useful because you get to know when the overall bundle size exceeds a certain threshold, although you don't get to know how much each part of the code is actually contributing to the final bundles. This is what we call analyzing the bundles, and in this recipe, you will learn how to use webpack-bundle-analyzer to audit the bundle sizes and the factors contributing to them.
The project we are going to work with resides in Chapter12/start_here/using-webpack-bundle-analyzer, inside the cloned repositor:
Now that we have built the app, let's see the steps of the recipe in the next section.
As you may have noticed, we have a main bundle of size 1.12 MB. This is because we are using the Three.js library and the moment.js library in our app.component.ts file, which imports those libraries, and they end up being in the main bundle. Let's start the recipe to analyze the factors for the bundle size visuall:
npm install --save-dev webpack-bundle-analyzer
{
...
"scripts": {
"ng": "ng",
"start": "ng serve",
"build": "ng build",
"test": "ng test",
"lint": "ng lint",
"e2e": "ng e2e",
"analyze-bundle": "webpack-bundle-analyzer dist/using-webpack-bundle-analyzer/stats.json"
},
"private": true,
"dependencies": {... },
"devDependencies": {...}
}
ng build --configuration production --stats-json
npm run analyze-bundle
This will spin up a server with the bundle analysis. You should see a new tab opened in your default browser, and it should look like this:
npm install --save date-fns
import { Component } from '@angular/core';
import { Router } from '@angular/router';
import { AuthService } from './services/auth.service';
import { format } from 'date-fns';
import { Scene } from 'three';
@Component({...})
export class AppComponent {
...
constructor(private auth: AuthService, private router: Router) {
const scene = new Scene();
console.log(format(new Date(), 'LLL do yyyy'));
}
...
}
Once webpack-bundle-analyzer runs you should see the analysis, as shown in the following screenshot. Notice that we don't have the moment.js file or the lib block anymore, and the overall bundle size has reduced from 1.15 MB to 831.44 KB:
Woohoo!!! You now know how to use the webpack-bundle-analyzer package to audit bundle sizes in Angular applications. This is a great way of improving overall performance, because you can identify the chunks causing the increase in the bundle size and then optimize the bundles.
18.116.118.198