
Good day to all. The release of Angular.js 2.0 is approaching, but the performance problems of the first version still remain. This article is devoted to the optimization of Angular.js applications and will be useful both for beginners and those who are already using this framework, but have not yet encountered problems with its performance.
Little simple theory
As you know, Angular.js is a declarative front-end framework providing convenient data binning. Of course, let's not forget about the testability, maintainability and conditional readability of Angular.js applications, but, in the context of this article, this is not important.
So, one of the features of this framework is convenient data binning "right out of the box." However, due to what it works? If simplified, the data binding in Angular.js is held by
scope ,
digest , and
watcher .
')
Scope (or $ scope) is an object containing data and / or methods that need to be displayed or used on the page, as well as a number of technical properties, such as its identifier, link to the parent scope, and so on.
A watcher is an object that stores the value of the expression specified by us and a callback function that needs to be called if this expression changes. The watcher array is in $ scope. $$ watchers.
Digest - alternately bypassing all watcher s and calling the calbek functions of those whose value has changed. If the digest results in at least one value being changed, the digest will be launched again. Therefore, often the digest is run two or more times. If the digest is launched more than 10 times, Angular will throw an exception.
Watchers are stored in a scope and you can see them by going through $ scope. $$ watchers. Basically they are created automatically, but they can be created manually. Directives use either the scope of the controller, or create their own. Accordingly, watcher s directives should be sought in their scope.
Obviously, the more watchers, the longer the digest cycle lasts. And since the javascript language is single-threaded, then with a significant digest duration, the application will start to slow down. Moreover, the digest is not just a traversal of the array, but also a call to the callbacks for those expressions whose value has changed. It is believed that angular ensures trouble-free operation as long as the page contains up to about two thousand watchers. And, although this figure sounds quite impressive, you can reach it quickly enough.
funny numbersYesterday my new record was set - I saw 65,000 votcherov on one page. And, as it seems to me, this was not the limit.
For example, this little piece of markup on ten lines will create
eighty watchers, plus
ten separate scope.
<table> <tbody> <tr data-ng-repeat="cartoon in model.cartoons" data-ng-class="{even: $even, dd:$odd}" data-ng-hide="cartoon.isDeleted"> <td>{{cartoon.no}}</td> <td>{{cartoon.name}}</td> <td>{{cartoon.description}}</td> <td>{{cartoon.releaseDate}}</td> <td>{{cartoon.mark}}</td> </tr> </tbody> </table>
And now to practice
The first performance problem lies in the plane of the number of watchers. And in order to solve it, we must clearly understand that by creating any expression of binding, we are creating a watcher. Ng-bind, ng-model, nd-class, ng-if, ng-hide, and so on — they all create an observer object. And, if, alone, they do not pose a threat, their use together with ng-repeat, as seen in the example above, can very quickly assemble an army of little killers of our application. And the biggest danger is the fully dynamic tables. It is they who are able to produce watcher-s on a scale worthy of Mr. Isiro Honda.
Therefore, the first (and sometimes the last) step in optimizing the number of watchers lies in analyzing the variability of the data that is displayed on the page. In other words, it is worthwhile to monitor only those data that must change. Very often there is a situation where the data just need to display the user. For example, you need to display a list of all possible purchases, or display static text, which neither the user nor the server will not affect. Therefore, in angular 1.3, a special syntax for one-time data binding has appeared, which looks like this:
data-ng-bind = ":: model.name"
or so
data-ng-repeat = "model in :: models"
This expression means that as soon as the data is calculated and displayed on the page, the
watcher responsible for this expression will be deleted . By combining a one-time binding with ng-repeat, you can get significant savings for watcher in our application. The truth is there is one nuance.
If there is no data involved in the binding expression (for example, the server sent null instead of the product name), then the watcher will not be deleted. It will "wait" for the data, and only then be deleted.
The second step is to share responsibility. In other words - not everything should be Angular. In the example above, the ng-class directive was used to set the CSS classes for even and non-even lines. Replacing it with the CSS tr: nth-child (even) rule, we will get rid of extra watchers, besides we will get a performance gain (extremely small) in speed. The situation is similar with events such as ng-mouseover and ng-mouseleave (their use causes other performance problems - which is discussed below). Often their processing can be assigned to its directive, plus jquery. Speaking of jQuery and directives. Sometimes, a table or list should be redrawn only in one or two cases. In this case, it will be much more efficient to use your directive with one or two manually created watchers. If any functionality does not cause the model to be redrawn, this is the first sign that it can be made not in the Angular style. This is not always necessary, but the decision must be made consciously.
I will give a simplified example. Let us have two lists of products - available, and those that are selected by the user. Obviously, the first list we will have, firstly, is large, and, secondly, static, since after it is loaded, neither the user nor the server will change it. So here we can use the “one-time” ng-repeat. But the second list is dynamic and constantly changing by the user. Therefore, we should not use one-time data binding here. Although, if we do not need actual data every second, but only at the moment when the “buy” button is pressed, then here too we can make static by placing the responsibility for collecting the final data on the directive. Do you need to spend resources on such optimization - see the current situation and the size of the lists.
And finally, the third step is to properly hide the unused markup. Out of the box, Angular.js provides ng-show / ng-hide that hide or show the parts of the page we need. However, the associated watchers do not disappear anywhere and participate in the digest, as before. But the use of ng-if completely cuts out elements from the Dom-tree together with the corresponding watchers. True, deleting elements from Dom is also not the fastest procedure, so using ng-if is worth those parts of the markup that will not be hidden / shown too often, where “too” depends on the particular application.
So, with multiple watchers, we sorted it out a bit. But a long digest is not the only stone that our application may stumble upon.
The second, albeit smaller, performance problem is that the digest is called too often.
Usually, problems with a digest arise when the number of watchers approaches a critical, but starting the digest too often can also create problems. As you know, a digest is not only traversing an array of watchers, but also performing callbacks for changed expressions. In addition, often the digest is run several times in a row, further slowing performance. Ng-model will start a digest after each letter entered. For example, entering this fifty-five letter word
Tetrahydropyranylcyclopentyl tetrahydropyridopyridine will trigger a digest at least one hundred and ten times. As soon as the user enters the first letter, a digest will be launched. Since during its execution it will be found that these models have changed, the digest will be executed again. By the way, the digest will be called not only on the scope of the controller, but also on the other scope of the page. Therefore, the ng-model can become quite a serious problem.
A simple solution would be to add a debounce parameter that defers the call to the digest for a specified time. The situation is similar with ng-mouseenter, ng-mouseover and so on. They may launch a digest too often, which will lead to a drop in application performance.
Therefore, special attention should be paid to areas with which the user can (albeit implicitly) invoke a digest in the application, such as input, areas of guidance, and so on. And if you need to call the digest manually, try to do it when the maximum number of changes is made, so that the digest picks up all of them in one pass.
And finally, the third problem is not very obvious features of the framework. Below is a list of interesting, in my opinion, moments that can also improve the performance of the application.
- If possible, use ng-bind instead of {{}}. The string binding expression is about twice as slow as compared to ng-bind. In addition, using ng-bind eliminates the need to use ng-cloak.
- Avoid using complex functions in binding expressions. The functions specified in the expression bindings are run each time a digest is started. And, since the digest is often run repeatedly, the execution of these functions can significantly slow down the rendering of the page.
- Use filters only if you cannot do without them. If the functions specified in the binding expression are executed once per digest, then the filter function is executed twice per digest, for each expression. It is best to filter the data in the controller or service.
- If possible, use $ scope. $ Digest () instead of $ scope. $ Apply (). The fact is that the first function will start the digest only within the scope on which it was called, and the second - in all scope, starting with the rootScope. Obviously, the first digest will go faster. By the way, $ timeout at the end will call $ rootScope. $ Apply ().
- Remember about the possibility of deferred calling a digest on the user's input when setting the debounce parameter: data-ng-model-options = "{debounce: 150}"
- Try to avoid using ng-mouse-over and similar directives. The call of this event will trigger a digest, and the nature of such events is such that they can be triggered many times in a short period of time.
- When creating your watchers, do not forget to save the function of their removal and call it as soon as the watchers are no longer needed. Also, avoid setting the objectEquality flag to true. This causes a deep copying and comparison of the new and old values to determine if the callback function should be called.
- You should not keep references to Dom elements in scope. It contains links to parent and child elements, i.e. in fact, the whole house element. And, it means that the digest will run through the whole Dom tree, checking which of the objects has changed. Needless to say, how expensive it is.
- Use the track by parameter in the ng-repeat directive. Firstly it is faster, and secondly it will prevent duplicates from an error in a repeater that is not allowed , which occurs when we try to output the same objects in the list.
This article is over. More details can be read on the links below:
Thank you for your attention, I hope your time has been well spent.