Building Performant Applications

A slow application is not a useful one. We developers can get complacent with our powerful machines and speedy fiber connections. We forget that most of the world connects through a phone, on a connection no better than 3G. Speed matters even more than ever—47% of users leave a page if it takes longer than 2 seconds to render.[8]

In this section, you’ll learn how to profile an Angular application and use observables to skip large swaths of unneeded performance issues.

Understanding Angular Performance

There are two types of performance to think about when digging through any frontend application: load and runtime. Load performance focuses on how quickly the application can get up and running for the user. Runtime performance is concerned with how quickly the application can respond to user input after it has loaded. While Angular provides many powerful tools to help optimize load performance (such as ahead-of-time compilation and service workers), these tools do not use observables and are outside the scope of this book. Angular’s runtime performance tooling uses nothing but observables, so I’m sure you’re ready to dive in.

Angular’s runtime performance boils down to one question: When something happens on the page (a click event or an AJAX request returning), how quickly can Angular make sure that all the data stored in models and displayed on the page is the correct information? This process is known as Change Detection. Angular’s predecessor, AngularJS, kept things simple by checking everything on every change. This ensured that no data would be stale, but created a hard limit for how much data could be checked before the web app started slowing down. Angular drastically changed things, optimizing both sides of the equation: when should I run change detection and what needs to be checked? The first half of the question was resolved with Zones.

Using Zones to Run Change Detection

Zones are a new concept to JavaScript, but they were a core language feature in Dart, a language Google created to replace JavaScript. Dart never gained traction, but zones made their way over to the JavaScript ecosystem to be a foundational part of Angular. In short, zones are a way to wrap asynchronous code into a single context. Let’s say you wanted to figure out how long an AJAX call took with regard to code execution time on the frontend. Something like this wouldn’t work:

 startTimer();
 ajax(url)
 .pipe(
  map(result => processResult(result)
 )
 .subscribe(
  () => stopTimer()
 );

The timer here would measure the entire scope of the request—the initiating code, the time spent waiting for the server to return the information, and the time spent processing the returned value. If the main thread is doing something at the moment the request returns, that is also tracked in the timer.

Instead, zones can wrap this call. A zone is only active when the code inside it is executing. Zones also allow us to write handlers that execute whenever the code contained within the zone begins or ends execution.

In Angular, these zones are used to wrap common frontend APIs like addEventListener and the XmlHTTPRequest object. This means Angular applications don’t need to write convoluted wrappers to be aware of all click events. Instead, Angular creates a new zone for all click events, and anytime that zone finishes executing, Angular runs change detection to see what’s been modified. Expand this to all events across an application, and Angular has a fine-grained idea of what’s going on within your application without additional effort on your part.

While it’s possible to create new zones, Angular sets up its own set of zones automatically, so you don’t need to.

Escaping the Context

images/aside-icons/note.png

Sometimes you might want to run something without triggering an entire change detection cycle. In this case, you’d need to run the code outside of Angular’s zone, so you can inject NgZone and use NgZone.runOutsideAngular(someFunction) to modify things.

When Angular determines that a change detection cycle needs to be run, the next task is to determine just what needs to be checked. Angular stores the state in a tree model, starting with the app-level component that mirrors how each component is placed in the DOM. Using the default settings, Angular starts at that root component and checks the various properties on that component, updating the DOM along the way as needed. Each component has its own independent change detector that runs during this process.

Profiling Change Detection

Now that you know what’s going on behind the scenes, let’s tap into those flows to see how observables are used to run the change detection. While digging through Angular’s internals, you’ll build some tooling to track the length of each change detection cycle. I’ve created an application that has some performance issues for you to debug in the bad-perf directory. Start that up with ng serve and look at the resulting page.

This application is a patient processing system for a hospital (using fake data generated by Faker.js[9])—certainly a situation where page response time matters. Fire it up and browse through the (fictitious) patients. You’ll notice that updating anything on the page takes a while. Certainly the page feels sluggish—but can we prove it?

Generate a new service with ng generate service cd-profiler. This service is in charge of tracking how long each change detection cycle takes and reporting it to you. Everything is orchestrated through Angular’s zone, NgZone, so the first order of business is to inject that into our service:

 constructor​(​private​ zone: NgZone) {

NgZone provides two key observable streams: onUnstable, which signals the start of change detection and onStable, signaling the end. We want to listen in on each one of these, mapping the results to add details about what type of event happened and what time the event took place. Add the following to the constructor in the service you just generated.

 const​ unstableLatest$ = zone.onUnstable
 .pipe(
  map(() => {
 return​ {
  type: ​'unstable'​,
  time: performance.now()
  };
  })
 );
 const​ stableLatest$ = zone.onStable
 .pipe(
  map(() => {
 return​ {
  type: ​'stable'​,
  time: performance.now()
  };
  })
 );

The Performance API

images/aside-icons/note.png

In this example, you’ll use the performance API, a tool provided by the browser that can provide more precision than just Date.now(). As of this writing, it works on the latest version of all modern browsers and Internet Explorer. If you encounter any trouble using it, replace any calls to performance.now() with Date.now().

Next, we’ll merge these two and add in the pairwise operator. pairwise is another operator that maintains internal state. On every event through the observable chain, pairwise emits both the newest event and the second-most-recent event. In this case, if the most-recent event is of type stable, we have all the information needed to determine how long the most-recent change detection cycle took. We don’t want to use the combineLatest constructor you learned about in Chapter 7, Building Reactive Forms in Angular, because that won’t preserve the ordering, and we only want to take action if the latest event was stable. Now, we’ll subscribe to this chain, logging the latest data to the console.

 merge(
  unstableLatest$,
  stableLatest$
 )
 .pipe(
  pairwise(),
  filter(eventPair => eventPair[1].type === ​'stable'​),
  map(eventPair => eventPair[1].time - eventPair[0].time)
 )
 .subscribe((timing) => {
  console.log(​`Change Detection took ​${timing.toLocaleString()}​ ms`​);
 });

Finally, add this service to the constructor for the root module in app.module.ts (don’t forget to import the service). Injecting it here is enough to get the service running, though it’ll run in every environment. If you use a service like this for profiling your own apps, make sure that it doesn’t run in prod.

 export​ ​class​ AppModule {
 constructor​ (​private​ cd: CdProfilerService) {}
 }

After this is all set up, reload the page. You should start seeing logs in the console detailing how long each change detection cycle took. Move a few patients around to trigger a few change detections. Note how long it took. Once you have details about the average length of a CD cycle, it’s time to start making improvements.

The first CD is triggered when you click the “Change Ward” button. This only updates a CSS class and is satisfactorily quick. On the other hand, when someone changes the patient data through the ward dropdown, it takes ages.

On a fairly modern computer, it takes almost two seconds to update a single patient. What’s going on?

Optimizing Change Detection

Every change triggers a noticable pause in our hospital patient app. Angular is fast, but the app is currently forcing it to do thousands of checks anytime a change is made. Like with any performance issue, there are multiple solutions, but in this case, we’ll focus on taking command of the change detection ourselves with the onPush strategy.

Each component comes with its own change detector, allowing us to selectively override how a component handles change detection. The onPush strategy relies on one or more observables annotated with @Input to do change detection. Instead of checking every round, components using onPush only run change detection when any of the annotated observables emit an event.

So what is this @Input annotation, anyway? Angular’s tree structure means that information flows from the top (app.component.ts) down through the rest of the components. We can use @Input to explicitly determine what information we want to pass down the tree. @Input annotates a property of our component (the property can be of any type that’s not a function).

Let’s open the row component that displays each row of patients. In the view (patient-row.component.html), you can see square brackets used to pass data about each patient to the patient-display component.

 <div class=​"row"​>
  <app-patient-display
  class=​"col-xs-2"
 *​ngFor=​"let patient of patientRowData"
 [​patient​]="​patient​"
  ></app-patient-display>
 </div>

The row component iterates over all of the patients it has, passing the individual patient data into a component built to render patient data. Angular knows the patient attribute is what’s used for the data, thanks to the following annotation in the patient component:

 export​ ​class​ PatientDisplayComponent ​implements​ OnInit {
  @Input() patient;

Without this annotation, Angular would not know to pass the data for the patient, and all the patient details components would be empty. However, @Input by itself does not optimize anything. It just says that some data can be passed through by some property. Next, let’s import ChangeDetectionStrategy and update the patient component to use onPush.

 @Component({
  selector: ​'app-patient-display'​,
  templateUrl: ​'./patient-display.component.html'​,
  styleUrls: [​'./patient-display.component.css'​],
  changeDetection: ChangeDetectionStrategy.OnPush
 })

This new strategy means that the component’s change detector only runs when the value that’s passed through by @Input changes. When the object in question is a regular, mutable object like it is currently, the change detector still needs to check the full equality of the object on every change detection cycle—a slow process. This is no way to increase performance. Instead, there are two options—make every patient object immutable (requires importing a third-party library) or use observables. When we annotate an observable with @Input, Angular handles it differently, treating every event in the observable stream as a “change” for the purpose of change detection. This allows us to precisely control when each cycle triggers, ensuring that no unneeded checks are run.

Joe asks:
Joe asks:
Why Not Use OnPush in Every Component?

Using OnPush as a change detector means that change detection runs only when an @Input property changes. This means that the component has no ability to modify its own internal state. Components like the patient component that only display data and don’t have any way to change their internal state are known as presentational components. A common application pattern is to have a parent container component that manages state and many presentational child components that merely handle rendering data to the page. This is why the Update Patient button is outside the patient display component.

For this to work, we need the row component to create an observable for the data of every patient. One way is to create an awkward method, using a BehaviorSubject so that the initial object is preserved for the component to subscribe to:

 // in the row component
 ngOnInit() {
 this​.patientData = ​this​.patientDataInput
  .map(​this​.createPatientObservable);
 }
 
 createPatientObservable(pData) {
 let​ patient$ = ​new​ BehaviorSubject();
  patient$.next(pData);
 
 return​ patient$;
 }

With this new update, the row component now passes an observable through to the patient component. The patient component, with its new change detector, only checks to see what’s new when that observable emits. It’s a bit awkward to create the list of observables with the createPatientObservable method, and it requires a lot of rewiring throughout the application. If you’re going to do a lot of rewiring anyway, it’d be better to switch to a tool suited to this problem: ngrx. ngrx is a tool you can use to control all of the state management in your application. This allows you to have more presentational components, further accelerating the application.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
3.135.216.75