In order to explain how the user experience can be made better along with 60 frames per second UI, a simplified business app is shown down below.
Here, is 2 lists of employees with a total of 140 entries.
Here it is seen that when one more entry was added, it processed slowly.
Why is it that slow?
In open chrome dev tools, by taking a look at the profiler, it is seen that the calculate method has been invoked very often inside the employee lists component.
Every time, the input is typed in, two events are triggered; key down and key up. For each event, the performance data is recomputed for each individual employee.
Ideas for optimization
The on push strategy will allow you to invoke the change detection in a given component subtree when you get new inputs for any of the component.
With on push change detection will be triggered when the framework, with reference check, determines that any of the inputs of a component has changed.
For instance, consider the employee list component as a function where;
- Inputs are the arguments of the function
- Rendered component is the result of the function
Down below is the nonangular code:
There is an employee component, which is represented by F here.
When the first time the data is passed, trigger the change detection is invoked as the reference to the input data is received, previously its value was undefined.
The next time that the data is pushed, the change detection will not be triggered. The data structure pointed out by the reference which is held by the data constant, is modified. If a copy is provided to this data structure, the change detection will be triggered.
Passing the new reference triggers the change detection.
The whole array cannot be copied every time that it is required to trigger a change detection.
There will be a lot of additional memory allocated which is not required and a lot of heavy computations are going to be performed- the entire list needs to be iterated in order to copy all the list items.
To solve this issue, Immutable JS can be used.
It is a collection of data structure developed by Facebook which provides two features;
- A new reference is received on change
- The entire data structure is not required to be copied
Down below is the result of implementing on push and immutable data on the app.
It got slightly faster but still not fast enough.
The difference of performance between the non-optimized app and the on push implemented app is shown graphically down below:
It is two times faster but still very slow.
Why is it still slow?
Here in the sales department, a new name is added.
Every time that the key is pressed, change detection is triggered at least twice for each employee and even the ones whose performances were already computed. However, here the change notation is only used for the sales department and not for both of them.
Only the data for the new employee needs to be computed and not the already existing employees.
The on push will trigger the change detection when an event in the component is triggered.
To fix this, refactoring is required.
This will not only make the app go faster but it will also separate our concerns.
Here, we have app component, employee list component which further breaks down to the name input component and list component.
Name input component is responsible for entering the names of new employees.
List component- where the individual employees are listed and have a function for calculating their performance data.
Here is how much faster it got.
The difference of performance between the non-optimized app and the refracted with on push implemented app is shown graphically down below:
Adding and removing items
When a new data is added, the performance data is recomputed, not only for the new item but for existing ones as well.
Everything is recomputed from scratch, every time a new data is entered.
In order to optimize it, down below is Fibonacci implementation is shown.
It has 2 properties. Firstly, there are no side effects and secondly, the same result is returned when they are invoked with the same set of arguments.
This type of functions are called pure functions.
In angular, there are pipes, which are known for data transformation. There are pure and impure pipes.
A decimal pipe can be the example of pure pipe and for impure pipe, it can be the pipe which holds some state.
Angular executes a pure pipe only when it detects a change to the input value. A pure change is either a change to a primitive input value (string, number, Boolean or symbol) or a changed object reference (Date, array, function, object).
Here is how it is done.
A simple calculate pipe is defined where the calculation is delegated to the business calculation and the list components are updated.
Instead of invoking the calculate method in the list components, the execution is delegated to the pipe.
Using bench press to bench press it and see how much faster it can go.
The difference of performance between the non-optimized app and the calculation in a pure pipe implemented app is shown graphically down below:
Initial rendering with 1000 items
The resources are downloaded from the network and now its taken time to be rendered is checked.
It took it 9.53 seconds to complete the initial rendering process.
To optimize it, the data needs to be looked at.
The example below shows a list of employees with a very similar data.
There is Fibonacci of 27, 28 and 29. During initial rendering same data is recomputed multiple times. The solution to this is applying caching which is also known as memorization.
It is possible for the pure functions because these functions do not produce side effects and they return the exact same results when applied to the same set of arguments.
Memoise can be used by loadash. Memoise can be invoked by the new function. Every time, the new function is invoked which is returned by memoise, the result is returned by the internal function which is going to be cached and the next time, it will be returned directly from the cache so it will not be needed to re-compute it.
Here is how much faster it went.
Everything is downloaded and again is waited to be rendered. This time it took 3 seconds less than it took earlier.
Why it is that memorization is producing better results than pure pipes?
With pure pipes, to calculate the result, the invocation is directly delegated to Fibonacci which returns the result.
With pure pipes, the angular will not re-evaluate these expressions on the next tick since the exact same arguments are passed to the calculate pipe.
But, with memorization, this technique is also used and along with that, the result is cached. This is a global cache.
On push performs memoisation because it memoise the result of the last rendered component subtree.
How NgForOf works
Ng4 is used quite a lot in order to iterate over the list.
It accepts a single argument in its type iterable differs. This implements an algorithm which based on two lists, returns a number of modifications and type of modifications between the lists.
When the change detection runs, ngDoCheck hook gets invoked. The difference between the current state of data structure that it is iterated over and its previous state.
Iterable differ checks whether the data structure has changed.
A data structure is implemented in order to keep track of the list of changes which have happened inside it. with mutation also applied, all the changes are pushed into the linked list.
Data structure optimized for angular
Custom differ is implemented. It only touches the changed in the linked lists. This reduces the complexity and enhances the performance.
Here are performance benchmarks with the new implementations.
There are other performance optimizations you can do to make your apps performant. Do you know any of those? Mention them in the comments below!