The Secret Life(cycle) of Components

There is an entire world of complex interactions happening just under the surface of an Angular application. We can use lifecycle hooks to perform complex state management, coordinate control flows and finesse asynchronous events that would normally result in unpredictable and volatile results.

🌴 This article was written in February 2017, after completing the migration from AngularJS to Angular v2. It was also covered in a talk at ngStockholm in March 2017

Solid foundation

When Angular creates a component, it calls the constructor at first. After this occurs, Angular will call on various lifecycle hooks throughout the usage of the component (everything from initialization to destruction).

The lifecycle hooks supported in Angular components are:

Hooks in sequence
  • ngOnChanges: called when an input or output binding value changes. Note that this will only be fired if a binding has actually changed state (ngOnCheck is called each time change detection is kicked off);
  • ngOnInit: only after the first ngOnChanges, but only once. It basically fires when the component is ready for use when all queries (content child and view child queries) and inputs have been resolved. It’s the place to perform complex initializations shortly after construction;
  • ngDoCheck: This is fired each time anything that can trigger change detection has fired (e.g. click handlers, http requests, route changes, etc…). This lifecycle hook is mostly used for debug purposes;
  • ngAfterContentInit: called after external content has been projected into the component. Remember content queries like @ContentChildren and @ContentChild are set before the hook callback is called;
  • ngAfterContentChecked: called after every check of component content;
  • ngAfterViewInit: called after component’s view(s) are initialized. Perfect for initializing event third party library, that needs the view be composed before taking any action (note that a view differs from content because a components view is the non-projected template);
  • ngAfterViewChecked: called after every check of a component’s view(s);
  • ngOnDestroy: called just before the component is destroyed. It’s important to be disciplined enough to start cleaning all jQuery and third part library component initialized and not used anymore.

There are a few important things to remember about lifecycle hooks:

  1. Angular’s unidirectional data flow rule forbids updates to the view after it has been composed, and anytime you don’t use enableProdMode() the app will complain about your changes. This basically means that you are not allowed to modify component binding data within any of lifecycle hooks. If you wish to do so then wrap the changes in a promise via Promise.resolve(() => { changes… }) .
  2. Dealing with view, content, queries or inputs within the constructor won’t work (use ngOnInit instead). The component constructor itself is used mostly for DI injection resolution.
  3. View data and content data exist in two different areas (the content is apart of the parent components template). This means that change detection will be processed in the content area first and then the view that houses the content.

When should we use them?

Let’s see how lifecycle hooks happen in relation to each other. But first is important understand how change detection works. Zone.js monkey patches most standard web APIs (like addEventListener, setTimeout, etc..) and every time an event handler or some other async code is completed, Angular runs change detection.

Just remember that the goal of change detection is always projecting data and its change.

Said that let’s see how lifecycle hooks are related to each other, and with the constructor.

  • constructor vs ngOnInit: be careful on the logic that you put in your constructor because may depend on that is not available yet. Data is not always immediately available in the constructor, so it should do no more than set the initial local variables to simple values. Divide all the initial logic between the three Init hooks. The ngOnInit is a good place for a component to fetch its initial data;
  • ngOnChanges vs ngDoCheck: as we said ngOnChanges is called when a value bound to an input has changed so you can run custom code when an input has changed. ngDoCheck to the other hand is called when change detection runs so you can implement your custom change detection action, but be careful on using it because will run all the time there is a change detection and user experience will suffer;
  • ngAfterContentInit vs ngAfterViewInit: use ngAfterContent when you want to use the power of Query like @ContentChildren ( Content projection is a way to import HTML content from outside the component and insert that content into the component’s template in a designated spot, also as known as transclusion). Instead use ngAfterViewInit for any initialization that needs the view be rendered like jQuery plugin or simple external DOM manipulations, this will avoid you use setTimeout in most of the other hooks;
  • ngAfterContentChecked vs ngAfterViewChecked: both hooks has to be used carefully and have a lightweight implementation because every time happen a change detection these hooks are going to be called;
  • ngOnChanges vs ngAfterViewChecked: there is mostly two type of components, the reusable one, and the unique one. The reusable one uses more ngOnChanges the unique could use also the power of ngAfterViewCheck. So it is important to understand when ngOnChanges fires and when ngAfterViewCheck fires. We will come back on that when we talk about bridge the gap between AngularJS and Angular.

Practical concepts

Let’s anchor this knowledge in practical concepts that you can start using in your projects right away. We are going to use as example Plan ( https://getplan.co ), a Project Execution Platform to help companies get the most out of their workforce.

Plan started as Backbone project in 2014, and when I joined as CTO in 2015, we begin a migration to Angular and ES6. The full migration ended early in January 2017.

Bridge the gap

During the migration between AngularJS (version 1.x) and Angular (version 2), the use lifecycle hooks slightly changed especially if you were abusing of $watch in AngularJS. It’s recommended to use ngOnChanges during your migration, but a small tip I can give, it is to start considering using ngAfterViewCheck in all case you have not a perfect unidirectional data flow and you need take action when a property in one of your services change state. Be careful that ngAfterViewCheck will run after every change detection and could impact your performance.

But definitely ngAfterViewCheck has been a very good friend during a long year of migrating and keep building at the same time features.

Speed up Performance in 7 steps

In the first couple of weeks of January 2017 we focus on debugging Plan’s performance before the final release with Angular version 2.4.2 .

Plan integrates a calendar with a todo list and hosting a page with 200 task, and each task owns a dozen of sub-component, the rendering can take quite a good time. Now in February our performance are pretty good, with loading a page of 200 tasks enough fast and seamless.

The Chrome DevTools Timeline shows a scripting time lower than a second and a scripting time between clicking to a new list till the list has been loaded less than 1 second. Keep this time in mind because now we are going to travel to few commits before.

Plan first week February 2017

This is how looked like Plan early in January, going to the same list with 200 tasks was very slow to open, and the performance in general was terrible, only the scripting was around 8 seconds.

Plan first week January 2017 (Disorganization and chaos to an absurd degree)

After that I start looking into a list of small improvements:

1) As first we stop initializing jQuery component of elements that are not showing up, in our case were the notes component inside the each task component that is used only if you collapse the task. Only this already speeds up to 4.2 seconds of scripting.

Plan first week January 2017, second iteration

2) Use Angular Animations helped to replace tons of jQuery plugins and unblock dozen of opportunities to increase the performance all over Plan.

3) Stop saving variable in ngOnInit when are not going to be used yet;

4) Stop using setTimeout inside ngAfterViewInit, because setTimeout will cause a change detection;

5) Moving all the jQuery initialization from ngOnInit in ngAfterViewInit;

6) Moving all logic that does not depend from the content but instead from the view from ngAfterContentInit to ngAfterViewInit;

7) Use ngOnDestroy to deallocate memory used from jQuery elements or removeEventListener .

All of this helped cut a speed up the performance and bring down to 2.6 seconds of scripting.

Plan first week January 2017, third iteration

This definitely was a vast improvement from 8 till 2.6 seconds, but something was keeping the overall User Experience sloppy as you can see in the gif.

Mastering all of them and knowing how to use them become crucial to unblock the max of Plan performance.

There is a deeper game

Till now we just touch the surface of a component lifecycle, to express the full potential of your app it’s time to start the final game. And see how your components and user experience react with the change detection.

To understand this game, as first it’s time to print out every change detection you generate by calling NgZone.run(callback).

To see visually how change detection will impact your performance game, we are going to use the tick buster.

The Tick Buster

A tick happens when ZoneTask.invoke has been called, and that will cause a change detection. We need to visualize when this tick has been called, and as first we had in a part of a template of our interest {{ tick() }} and after that inside the interested component a simple console.log.

The change detection has also been called by every setTimeout, so we are going to print out all of them, and showing what arguments have been passed. Reading the arguments like delay and extra ones will help us understand where is located a specific one.

Here recorded a typical iteration of moving the mouse around the page.

The tick() (aka Tick Buster) has been fired too many times, and mostly don’t need it. Why was that happening?

There were several leaking mousemove and click handler made slow down Plan drastically. More were the tasks in the page, and more change detection was caused, creating a poor user experience. In our case, the mousemove in FullCalendar was causing firing the Tick Buster all the time.

All these cases happen in with: mousemove, mouseup, resize, setTimeout, setInterval, requestAnimationFrame. Not forget using Promise will cause setTimeout too. In all of them remember how Zone.js monkey patches eachaddEventListener and calls ZoneTask.invoke causing change detection.

It’s time to going into the Twilight Zone and see how we can optimize Zone.js to don’t doing that.

Twilight Zone

Manipulate Zone.js events, and 3rd party events will help us optimize all the performance drastically.

Fortunately, Angular provides us a solution. We can inject the NgZone for executing work inside or outside of the Angular zone. The most common use of this service is to optimize performance when starting a work consisting of one or more asynchronous tasks that don’t require UI updates or error handling to be handled by Angular. Such tasks can be kicked off via runOutsideAngular which won’t trigger the change detection mechanism.

Using a debugger inside our tick() function will help us bust all the setTimeout and action that cause extra change detection.

When the debugger runs, it’s important focus our attention to ZoneTask.invoke and check what inside the self variable the value of source and data.target.

If running a debugger feels a little annoying you can take a step forward and add one line of code in the ZoneTask inside Zone.js.

The single line of console.log will print out the source and target of where has been invoke zone.

Our tick() is inside the task component, and as aspected should run three times, instead when I click on the page, it’s been invoke 9 times ZoneTask.invoke, causing multiple times change detection. The source says there are several case of a Click handler on the document. That was caused by the Angular host hook.

To use the power on NgZone we move the code into ngAfterViewInit and ngOnDestroy, so onClickDocument via runOutsideAngular will escape run outside Angular's zone and will not trigger Angular change detection.

Now most of the extra tick have been removed, and the change detection happen only for two event handler.

What we do now?

Let’s used this knowledge and keep busting more case like this one to increase the speed. The final result is very fluid Plan app and our page with 200 task loading in less than one second. ✌️

Grazie

Ciao everyone, I am Leonardo Zizzamia, and I am origin from Taranto South Italy and now my new home is San Francisco from four years already. I don’t write often, actually never, and has been an honor and a pleasure write this article for the NgVikings conference. I want say Grazie (thank you) to Lukas Ruebbelke and Matias Niemelä for the help and support to research and editing this article.

Un abbraccio! 😄

For extra questions, send me a tweet or a private message at Twitter @zizzamia. And if you’d love to build a product with a great team, we are hiring at Plan!

Reference