I'm evaluating whether or not to use AngularJS for a web project, and I'm worried about the performance for a feature I need to implement. I would like to know if there's a better way to implement the functionality I'm trying to in AngularJS.
Essentially, it seems to me the time it takes AngularJS to react to an event is dependent on the number of DOM elements in the page, even when the DOM elements aren't being actively changed, etc. I'm guessing this is because the $digest function is traversing the entire DOM.. at least from my experiments, that seems to be the case.
Here's the play scenario (this isn't exactly what I'm really trying to do, but close enough for testing purposes).
I would like to have angularJS highlight a word as I hover over it. However, as the number of words in the page increases, there's a larger delay between when you hover over the word and when it is actually highlighted.
The jsfiddle that shows this: http://jsfiddle.net/czerwin/5qFzg/4/
(Credit: this code is based on a post from Peter Bacon Darwin on the AngularJS forum).
Here's the HTML:
<div ng-app="myApp"> <div ng-controller="ControllerA"> <div > <span ng-repeat="i in list" id="{{i}}" ng-mouseover='onMouseover(i)'> {{i}}, </span> <span ng-repeat="i in listB"> {{i}}, </span> </div> </div> </div>
Here's the javascript:
angular.module('myApp', []) .controller('ControllerA', function($scope) { var i; $scope.list = []; for (i = 0; i < 500; i++) { $scope.list.push(i); } $scope.listB = []; for (i = 500; i < 10000; i++) { $scope.listB.push(i); } $scope.highlightedItem = 0; $scope.onMouseover = function(i) { $scope.highlightedItem = i; }; $scope.$watch('highlightedItem', function(n, o) { $("#" + o).removeClass("highlight"); $("#" + n).addClass("highlight"); }); });
Things to note: - Yes, I'm using jquery to do the DOM manipulation. I went this route because it was a way to register one watcher. If I do it purely in angularJS, I would have to register a mouseover handler for each span, and that seemed to make the page slow as well. - I implemented this approach in pure jquery as well, and the performance was fine. I don't believe it's the jquery calls that are slowing me down here. - I only made the first 500 words to have id's and classes to verify that it's really just having more DOM elements that seems to slow them down (instead of DOM elements that could be affected by the operation).
Usually, if your app is becoming slow, the main reason for it is too many watchers. AngularJS uses dirty checking to keep track of all the changes made in the code. If two watchers are interlinked, the digest cycle runs twice to ensure that all the data is updated.
When it comes to do DOM manipulation, binding event, etc... It happens, that we define functions that manipulates the DOM in a custom directive's link function, but we call it from the controller (we define functions in the $scope so it can be accessible by the given controller).
Angular Ivy is a new Angular renderer, which is radically different from anything we have seen in mainstream frameworks, because it uses incremental DOM.
Although an accepted answer exists allready, I think its important to understand why angularJS reacts so slow at the code you provided. Actually angularJS isnt slow with lots of DOM elements, in this case it's slow because of the ng-mouseover directive you register on each item in your list. The ng-mouseover directive register an onmouseover event listener, and every time the listener function gets fired, an ng.$apply() gets executed which runs the $diggest dirty comparision check and walks over all watches and bindings.
In short words: each time an element gets hovered, you might consume e.g. 1-6 ms for the internal
angular dirty comparision check (depending on the count of bindings, you have established). Not good :)
Thats the related angularJS implementation:
var ngEventDirectives = {}; forEach('click dblclick mousedown mouseup mouseover mouseout mousemove mouseenter mouseleave keydown keyup keypress submit focus blur copy cut paste'.split(' '), function(name) { var directiveName = directiveNormalize('ng-' + name); ngEventDirectives[directiveName] = ['$parse', function($parse) { return { compile: function($element, attr) { var fn = $parse(attr[directiveName]); return function(scope, element, attr) { element.on(lowercase(name), function(event) { scope.$apply(function() { fn(scope, {$event:event}); }); }); }; } }; }]; } );
In fact, for highlighting a hovered text, you probably would use CSS merely:
.list-item:hover { background-color: yellow; }
It is likely that with newer Angular Versions, your code as is, will run significantly faster. For angular version 1.3 there is the bind-once operator :: which will exclude once-binded variables from the digest loop. Having thousands of items, exluded will reduce the digest load significantly.
As with ECMAScript 6, angular can use the Observe class, which will make the dirty comparisiion check totaly obsolete. So a single mouseover would result internally in a single event callback, no more apply or diggesting. All with the original code. When Angular will apply this, I dont know. I guess in 2.0.
I think that the best way to solve performance issues is to avoid using high level abstractions (AngularJS ng-repeat with all corresponding background magic) in such situations. AngularJS is not a silver bullet and it's perfectly working with low level libraries. If you like such functionality in a text block, you can create a directive, which will be container for text and incapsulate all low level logic. Example with custom directive, which uses letteringjs jquery plugin:
angular.module('myApp', []) .directive('highlightZone', function () { return { restrict: 'C', transclude: true, template: '<div ng-transclude></div>', link: function (scope, element) { $(element).lettering('words') } } })
http://jsfiddle.net/j6DkW/1/
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With