Speeding up angular.js or how not to shoot yourself in the foot



    Good day to all. The Angular.js 2.0 release is approaching, but the performance issues of the first version still remain. This article is devoted to optimizing Angular.js applications and will be useful for both beginners and those who already use this framework, but have not yet encountered problems with its performance.

    A bit of simple theory


    As you know, Angular.js is a declarative front-end framework that provides convenient data binding. Of course, let's not forget about the testability, supportability 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 the convenient data binding “out of the box”. However, due to what does it work? In simple terms, data binding in Angular.js is based on scope , digest , and watcher .

    Scope (or $ scope) is an object containing data and / or methods that will need to be displayed or used on the page, as well as a number of technical properties, such as its identifier, a link to the parent scope, and so on.

    Watcherit is an object that stores in itself the value of the expression we specified and the callback function that needs to be called if this expression changes. The array of watchers is in $ scope. $$ watchers.

    Digest - alternately bypassing all watchers and calling the callback functions of those whose value has changed. If as a result of the digest, at least one value has been changed, the digest will be launched again. Therefore, often the digest is launched two or more times. If the digest is run more than 10 times, Angular will throw an exception.

    Watchers are stored in scope and you can see them by going through $ scope. $$ watchers. Basically, they are created automatically, but they can also be created manually. Directives use either the scope of the controller or create your own. Accordingly, watcher directives should be sought in their scope.

    Obviously, the more watchers, the longer the digest cycle. And, since the javascript language is single-threaded, then with a significant digest duration, the application will begin to "slow down". Moreover, a digest is not just a traversal of an array, but also a callback call for those expressions whose value has changed. It is believed that angular guarantees trouble-free operation as long as the page contains up to about two thousand watchers. And, although this figure sounds quite impressive, it can be achieved quickly enough.

    funny numbers
    My new record was set yesterday - I saw 65,000 watchers on one page. And, as it seems to me, this was not the limit.

    For example, this little piece of ten-line markup will create eighty watchers plus ten separate scope

    {{cartoon.no}}{{cartoon.name}}{{cartoon.description}}{{cartoon.releaseDate}}{{cartoon.mark}}

    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 creating any binding expression, we create 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, then using them 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. They are the ones who are able to produce watchers on a scale worthy of Mr. Isiro Honda.

    Therefore, the first (and sometimes the last) step in optimizing the number of watchers lies in the analysis of the variability of the data that is displayed on the page. In other words, it is worth monitoring only the data that should change. Very often a situation arises when the data just needs to be displayed to the user. For example, you need to display a list of all possible purchases, or display static text that neither the user nor the server will affect. Therefore, a special syntax for one-time data binding appeared in angular 1.3, 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 counted and displayed on the page, the watcher responsible for this expression will be deleted . By combining one-time binding with ng-repeat, you can get significant watcher savings in our application. True, there is one caveat. If there is no data involved in the binding expression (for example, the server sent null instead of the product name), then watcher will not be deleted. It will “wait” for data, and only then 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 CSS classes to even and odd lines. Replacing it with the CSS tr: nth-child (even) rule, we will get rid of unnecessary watcher-s, besides we get a gain (extremely small) in speed. The situation is similar with events such as ng-mouseover and ng-mouseleave (using them also causes other performance problems - more on that below). Often their processing can be assigned to their 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 together with one or two manually created watchers. If any functionality does not cause the model data to be redrawn, this is the first sign that it can be done not in 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 have will be, firstly, large, and, secondly, static, since after loading it, 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, here we should not use a one-time data binding. Although, if we do not need up-to-date data every second, but only at the moment of clicking on the “buy” button, here you can also make a statics, assigning responsibility for collecting the final data to the directive. Whether it is necessary to spend resources on such optimization - see the current situation and the size of the lists.

    And finally, the third step is to correctly hide the unused markup. Out of the box, Angular.js provides ng-show / ng-hide which 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, removing elements from Dom is also not the fastest procedure, so using ng-if is to those parts of the markup that will not be hidden / shown too often, where “too” depends on the particular application.

    So, with a few watchers we figured out a bit. But a long digest is not the only stone that our application can stumble on.

    A second, albeit smaller performance issue is calling the digest too often.

    Usually, digest problems arise when the number of watchers approaches a critical one, but starting the digest too often can also create problems. As you know, a digest is not only a bypass of the watcher array, but also callbacks for changed expressions. In addition, often the digest starts several times in a row, further slowing down the performance. Ng-model will run a digest after each letter entered. For example, the entry of this word from fifty-five letters Tetrahydropyranylcyclopentyltetrahydropyridopyridinewill launch a digest at least one hundred and ten times. As soon as the user enters the first letter, a digest will be launched. Since in the process of its execution it will be discovered that the model data has changed, the digest will be repeated. By the way, the digest will be called not only on the scope of the controller, but also on other scope pages. Therefore, ng-model can be a pretty serious problem.

    A simple solution is to add a debounce parameter that will delay the call of the digest for the specified time. A similar situation using ng-mouseenter, ng-mouseover and so on. They can run the 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 not explicitly) call the digest in the application, such as input, guidance areas, 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 them all up in one go.

    And finally, the third problem is the not too obvious features of the framework. Below is a list of interesting, in my opinion, points that can also improve application performance.

    • If possible, use ng-bind instead of {{}}. The binding string expression is processed about twice as slow compared to ng-bind. In addition, the use of ng-bind eliminates the need to use ng-cloak.
    • Avoid using complex functions in binding expressions. The functions specified in the binding expression are run each time the digest starts. And, since the digest is often run repeatedly, the implementation 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 two times per digest, for each expression. It is best to filter data in the controller or service.
    • If possible, use $ scope. $ Digest () instead of $ scope. $ Apply (). The fact is that the first function will launch the digest only within the scope on which it was called, and the second - on all scope, starting with rootScope. Obviously, the first digest will be faster. By the way, $ timeout at the end will call exactly $ rootScope. $ Apply ().
    • Keep in mind the possibility of deferred calling a digest on user input by 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 deleting them and call it as soon as the watchers are no longer needed. Also, avoid setting the objectEquality flag to true. This causes a deep copy and comparison of the new and old values ​​to determine whether a callback function call is needed.
    • Do not store links to Dom elements in scope. It contains links to parent and child elements, i.e. essentially an entire house element. And that means that the digest will run through the entire Dom tree, checking which of the objects has changed. Do not say how expensive it is.
    • Use the track by parameter in the ng-repeat directive. Firstly, it’s faster, and secondly it will save duplicates in a repeater are not allowed from the error , which occurs when we try to display the same objects in the list.

    This article is over. More details can be found on the links below:


    Thank you for your attention, I hope your time has been well spent.

    Also popular now: