Route-Fully-Rendered Detection in Angular
Tags: angular, typescript, performance, rxjs
Published: 2019-11-04 22:52:26

---
title: Route-Fully-Rendered Detection in Angular
published: true
description: Profiling your apps performance is important for any team. But how can you do it without majorly intrusive code changes? Come see what we were able to do in the name of performance profiling. 
tags: angular, typescript, performance, rxjs
---

![Alt Text](https://thepracticaldev.s3.amazonaws.com/i/xaxwdp6sm0cqpydsb21u.jpeg)

## Background

One of the main pieces of profiling an app's performance is measuring how long it takes to route from one view to another *(_more comments at end_).  If you can track over time your app's view-to-view rendering times, then you get the powerful, God-like ability to see at-a-glance when any of your routes have slowed down unexpectedly. 

## What are you measuring?

When you're measuring the time it takes to route from one view to another, what are you really measuring? While we could spend an afternoon discussing what the most important pieces to measure are, I am going to take another angle. Let me share the user story that I needed to solve. 

> As a user, I want to measure the time it takes from when I __start to navigate__ until the next page is __fully rendered__. 

So once the navigation starts, the user wants to know how long it takes until the next view is fully rendered.  Great! Now that I know what I want to measure, how do I do this in Angular? Let's talk about how I detect these two distinct moments: Navigation Start and View Fully Rendered. 

## Detecting "Navigation Start"

All frontend frameworks that have a router make detecting the beginning of the navigation easy. Angular is no exception. In Angular, we can use the `Router` for this. The following is a code block that we can run to grab only `NavigationStart` events, and then track when that happens.

```typescript
@Injectable({providedIn: 'root'})
export class PerfService {

    navStart$ = this.router.events.pipe(
        filter( event => event instanceof NavigationStart),
        startWith(null), // Start with something, because the app doesn't fire this on appload, only on subsequent route changes
        tap(event => /* Place code to track NavigationStart here */),
    ).subscribe();

    constructor(private router: Router) {}

}
```

In the above code, I am successfully detecting the start of navigation. Easy!

Now let's look at what we need to do when the next view is fully rendered. 

## Detecting "View Fully Rendered"

Angular doesn't provide a way for us to intrinsically know that a view has fully rendered. There isn't a lifecycle hook in Angular or React that says _"All Things Accounted For, I am Fully Rendered!"_. The closest thing we have in Angular is `AfterViewInit`, which only tells you that the initial view of the component has been rendered. Any requests to the server will blow this away. 

So now what?

### A Too Simple Approach

The simplest approach to detecting when a view is fully rendered is to look at its code, and once all of the data has been retrieved and set onto your component, you can then say that this view is fully rendered. 

But think about that...

You have to modify every single component in your app to detect and report its `Fully Rendered` event. That means three things: 

1. You have to modify every top-level component in the app and teach it to report this event. 
2. You have to create an audit so that no one on your team has forgotten to report this event on any future routes. 
3. The code that reports this `Fully Rendered` action is spread throughout your entire application. 🤢

This was not a good enough solution. So let me tell you what I did. I wanted something that didn't require me to modify every component manually and distribute the detection code across the app. I wanted a federated approach that allowed all of the detection code to be collected into one place. So how can I do that?

### An Accurate Approach to _Fully Rendered_

`zonejs` to the rescue! One great things about working with Angular is that you already have `zonejs` in your app. And for this problem, `zonejs` is PERFECT!!

When anything noteworthy happens in an Angular app, the `NgZone` will tell you that it is not stable. And once all of the noteworthy things have finished, it will tell you that it is stable again. So, once the `NavigationStart` fires, you can ask the `NgZone` to tell you once all of the noteworthy things have finished happening. This way you can distinguish between your app actively rendering the next view and when the app is done retrieving data and rendering the next view. SO COOL!

Anything _noteworthy_ in Angular will create whats called a __macrotask__ in the Angular zone. Any time we fetch new data or the component is deciding if it needs to re-render, a macrotask is created in the zone. So we can ask the zone if it has any macrotasks pending.

_Note: I didn't use `zone.isStable` because sometimes the `zone.isStable` will return true, even when we have pending macrotasks. So it's better to look at the pending macrotasks._

Once the `NavitationStart` fires, you can use some code like this:

```typescript
// Once NavigationStart has fired, start checking regularly until there are
// no more pending macrotasks in the zone

// Have to run this outside of the zone, because the call to `interval`
// is itself a macrotask. Running that `interval` inside the zone will
// prevent the macrotasks from ever reaching zero. Running this outside
// of Angular will not track this `interval` as a macrotask. IMPORTANT!!
this.zone.runOutsideAngular(() => {
    // Check very regularly to see if the pending macrotasks have all cleared
    interval(10)
        .pipe(
            startWith(0), // So that we don't initially wait
            // To prevent a memory leak on two closely times route changes, take until the next nav start
            takeUntil(this.navigationStart$),
            // Turn the interval number into the current state of the zone
            map(() => !this.zone.hasPendingMacrotasks),
            // Don't emit until the zone state actually flips from `false` to `true`
            distinctUntilChanged(),
            // Filter out unstable event. Only emit once the state is stable again
            filter(stateStable => stateStable === true),
            // Complete the observable after it emits the first result
            take(1),
            tap(stateStable => {
                // FULLY RENDERED!!!!
                // Add code here to report Fully Rendered
            })
        ).subscribe();
});

```

## _Voalá_

At this point, we have detected both the `NavigationStart` as well as the `Fully Rendered` moments. We can now use something like `PerfumeJS` to begin measuring these times. Once `PerfumeJS` measures the times, we need to stick those into our analytics database so that we can begin to track these over time. 

Viewing this data over time on a per-route basis allows you to understand you apps in very meaningful ways. If an API ever slows down, the views affected will load slower, and this test will tell you. If someone checked in some bad code that is killing the app performance, this will tell you. 

I invite everyone to begin checking out their apps performance and using approaches like this to do that. 

#### Afterthought

In your app, if you have any long running `setInterval` calls then it is possible that the `macrotasks` never settle in your app. If this method doesn't work for you, then you have other issues in your app. It is very easy to find a third-party component or library that will have a `setInterval` that will prevent your app's zone from ever settle back to zero pending macrotasks. How to find and fix these issues is the topic of another blogpost, another day. If anyone is in this situation and would like to chat about getting it fixed, please DM me on twitter :)

#### Extra Thoughts
__*__ Regarding the most important things to measure on your apps performance... there are a lot of things to measure. Things like _time to first byte_ or _time to first meaningful content_. This post is not to declare that this is the most important piece of frontend performance measuring. It is, however, something that should matter when you look at how your app is performing.