AngularJS ng-repeat performance - angularjs

I use AngularJS ng-repeat in order to view my table elements (it shouldn't be used very often - I know - but I don't know how to do it in an other way)
Here my example how I'am showing the containerObjects in table:
http://jsfiddle.net/NfPcH/10390/
ng-repeat=...
I have a lot containedObjects (with start, end and containerType) (around 600 per page) which are shown in table.
It took about 3 Seconds to show the view.
My question now would be, if something can be improved in order to improve performance. Is there a possibility to replace/change ng-repeat to decrease loading time.
Thanks a lot!
[EDIT]
I also have this function invocation but I have no idea how to prevent the function invocation. Does have anyone any idea how I could improve this?
Thanks a lot !
ng-repeat="serviceSchedule in getServiceScheduler(institutionUserConnection)">
function getServiceScheduler(institutionUserConnection) {
if(institutionUserConnection.scheduler != null) {
var serviceSchedules = institutionUserConnection.scheduler.serviceSchedules;
return serviceSchedules[Object.keys(serviceSchedules)[0]];
} else {
return null;
}
}

Improvement #1:
As #Petr Averyanov suggested, use a variable instead of a function call.
instead of:
ng-repeat="serviceSchedule in getServiceScheduler(institutionUserConnection)"
change your logic to:
ng-repeat="serviceSchedule in preCalculatedServicesObjectInScope"
To do this, you need other logic changes in controller, you have to manage when the result of getServiceScheduler(institutionUserConnection)" actually changes yourself but it WILL increase the performance.
Improvement #2:
Use variables that are evaluated once if values of rows never change once they are rendered. This will reduce watchers drastically and help you improve overall performance:
```html
<tr ng-repeat="institutionUserConnection in someScopeVariable">
<td>{{ ::institutionUserConnection.user.firstname }} {{ ::institutionUserConnection.user.lastname }}</td>
</tr>```
Improvement #3:
Use track by statement in ng-repeat. Angular.js normally checks/tries to identify which object is which using a special hash function $id in ng-repeat. However, you naturally have an identifier for each object in array (and you should) you can make ng-repeat use this instead of creating it's own id. For example:
<tr ng-repeat="x in someScopeVariable track by x.id">
Improvement #4: (getting hackier here)
Whatever you do, since your array is large, you may not be able to increase the performance enough. Then you should ask this question, do people really see 600 items in one look? No, it won't fit to page. So, you can chop the part that doesn't fit the page, reducing DOM elements to be rendered at once. To do that you can use limitTo filter:
<tr ng-repeat="x in someScopeVariable | limitTo: visual.limitValue">
You can make visual.limitValue a reasonable count such as 10 in the beginning and you can increase it on table's scroll to increase it when use is about to reach the bottom, resulting partial appending of DOM to page. However this hack requires both style and code changes in the page.
Improvement #5: (hackiest one, but it works)
If dimensions of each row is constant, using scroll position and dimension of rows, you can calculate which rows should be visible and you can render them only. I've written a directive for practice, named it uber-repeat and tested it with 60.000 rows and performance was amazing, even in mobile phones!
I think there is a project uses somewhat same approach here: angular-vs-repeat

Related

$watcher count exploding in ng-repeat virtual scolling

I am trying to implement a virtual scrolling tree directive in Angular using this guide as a reference. However, when I start scrolling, the $watcher count explodes to 17k-20k+ watchers (which crashes the page), scrolling is consistently slow, and nothing I have tried seems to help.
Plunker with my current code: HERE
(Note, above not showing up for me in Firefox, but is working in Chrome). If you have any thoughts of what else I can try so the scrolling is not a disaster, I am open for ideas. Been working on this for way too long...
Other methods I have tried:
$compile(element.components())(scope.$new())
Was called in onScroll(). Result: The list no longer displayed at all/still lagged badly and I got continual "Cannot call method 'insertBefore' of null on $compile" errors until the page crashed.
<li ng-repeat="node in nodeList" vs-node="node"></li>
Aka, I tried to give each element an isolated scope of its own in hopes that its scope and any watchers associated with it would be destroyed when the list was updated. Result was no difference with the watcher issue of above.
function clearVisibleProvider(nodeList){
for(var i=nodeList.length-1; i >= 0; i--){
nodeList[i] = null;
}
return nodeList;
function clearNode(node){
if(node.nodes){
for(var j=node.nodes.length-1; j >= 0; j--){
clearNode(node.nodes[j]);
}
}
nodeList[i] = null;
}
}
An attempt to clear old list elements before replacing them. Again, no difference. It was called within updateDisplayList() before the main list was updated.
Achieved my goal by redesigning to use a regular list view instead. Displayed nodes are flattened into a list, manual indexing is used to keep track of things, and there is no nesting. Any elements that would be nested have padding calculated based on their level of depth in the tree.
There was also a bug where I was getting too many nodes at once as well. Instead of 50, I'd get 200-500. Fixing that did not solve the watch count exploding however, so I don't consider it the source of the original problem.

AngularJS arrays, proto and .length - Why can't i get a valid number?

Sometimes you need to ng-if or ng-show an item in html based on some choices made earlier. One of these for me is "Additional Item". You can enter one set of information, and also if you want, an additional set. This creates an array of 2 similar objects. With this setup, you can only have 1 or 2 objects in this array. (important, since the scope of this question needs to be limited this way)
I want to ng-show an html directive based on "myItemsArray.length > 1". Since the array can (read should) only be 1 or 2 in length (not 0), this should work. However, it does not, because AngularJS seems to be adding an item "proto" to the array which adds to the count. See the image.
The problem is, proto makes the array length equal 2. I am not going to just look for length > 2 because i really don't know if i can count on proto always being there, and i just think thats bad practice anyway.
Also, i know there are MANY other ways of doing this (setting a boolean, or using another var to indicate etc, but i really just want to work with count of items in the array because "business logic"..
EDIT:
After doing a little debugging, i'm seeing that i have an array of "Object, undefined". How is this even possible :)
Some search lead me to this. Why are some values ​​in my array undefined
EDIT:
Seems that using a delete may cause this problem

AngularJS shuffle and limitTo a list

I want to use AngularJS to display a shuffled list - but only the first couple of elements. At the moment, I perform both the shuffling and limiting in the HTML template, demonstrated in this fiddle:
<li ng-repeat="value in array | shuffle | limitTo:1">
{{value}}
</li>
This works fine, but causes Angular to exceed 10 $digest iterations when there are more items in the list than are shown, as in the example. What is happening, as far as I can tell, is that something is watching the filtered and limited value of the list, which is highly likely to change when there all the elements will not always be displayed.
How should I achieve this effect without breaking Angular? Of course, it still works like it should, but it's inefficient and probably an indication that what I'm doing is incorrect and should be achieved some other way.
The obvious solution is to shuffle the list in the controller before it is ever displayed - but I do want the selection of elements displayed to change each time the user updates the view.
EDIT: here's a better example of what I'm trying to achieve - the app now lets you switch between two lists, which get shuffled and limited each time.
As mentoined before - angular waits for an expression gets stabilized.
So, the best way, is to shuffle an array before passing it to view.
Anyway, there are some tricks, that would help you to keep it clean.
http://jsfiddle.net/zc3YH/3/
In this fiddle we cache shuffle result, while a length keeps the same. So, you get array reshuffled not when an array changes, but when it length changed. You can implement more complex caching behavior. So, the main idea is to shuffle the same array only once, and reshuffle it only on array update.
Current filter implementation is really bad, cause it caches only single array, so if you would use this filter twice - it would be broken. So, caching should be done using something like hashKey for an array to differ different arrays.
shufflemodule.filter('shuffle', function() {
var shuffledArr = [],
shuffledLength = 0;
return function(arr) {
var o = arr.slice(0, arr.length);
if (shuffledLength == arr.length) return shuffledArr;
for(var j, x, i = o.length; i; j = parseInt(Math.random() * i), x = o[--i], o[i] = o[j], o[j] = x);
shuffledArr = o;
shuffledLength = o.length;
return o;
};
});
Anyway, it is not good practice to modify data in scope within a filter. If you need this shuffled array be shared - shuffle it in your controller/service/.... So, var o = arr.slice(0, length) copies that array, thus we keep originar array unmodified.
This fiddle demonstrates the 'filter in the controller' approach. Instead of actually creating a filter, shuffling is defined as a regular function and applied to $scope.array.
I think tgat problem is that your filter modify source array. Even if you return new array it would be asane problem. Its how dirty checking works: it keeps evaluating expression until it gets "stabilized" (or 10 iterations). More isdescribed in this post: https://groups.google.com/forum/m/#!msg/angular/IEIQok-YkpU/InEXv61MrkMJ
Solution would be pre-shuffle values in controller so expression would return same vallue each dirty-check phase... Sorry, but that is the best way for now (unless you whant to play dirty games with $$hash to let ngRepeat beleave that yourexpression evaluates into same values :))

Angular JS ng-repeat consumes more browser memory

I have the following code
<table>
<thead><td>Id</td><td>Name</td><td>Ratings</td></thead>
<tbody>
<tr ng-repeat="user in users">
<td>{{user.id}}</td>
<td>{{user.name}}</td>
<td><div ng-repeat="item in items">{{item.rating}}</div></td>
</tr>
</tbody>
</table>
users is an array of user objects with only id and name. number of user objects in array - 150
items is an array of item objects with only id and rating. number of item objects in array - 150
When i render this in browser, it takes about 250MB of heap memory when i tried profiling in my chrome - v23.0.1271.95.
I am using AngularJS v1.0.3.
Is there an issue with angular or am i doing anything wrong here?
Here is the JS fiddle
http://jsfiddle.net/JSWorld/WqSGR/5/
Well it's not the ng-repeat per se. I think it's the fact that you are adding bindings with the {{item.rating}}.
All those bindings register watches on the scope so:
150 * 2 = 300(for the 2 user infos)
150 * 150 = 22500(for the rating info)
Total of 22800 watch functions + 22800 dom elements.
That would push the memory to a conceivable value of 250MB
From Databinding in angularjs
You can't really show more than about 2000 pieces of information to a
human on a single page. Anything more than that is really bad UI, and
humans can't process this anyway.
I want to say the leak is in the second array because you are potentially looping through the same array and displaying every item for every user row in users so depending on how large your test data is that view could get rather large. I could do a little more investigating. btw your fiddle is something entirely different.
Right now you are looping through 150 X 150 = 22500 items. And registering a watch (or through a directive just adding item rating) to each one.
Instead - consider adding the user's rating to the user object itself. It will increase the size of each user object but you will only loop through 150 items and register watches only on them.
Also - consider looking into Indexes. It's apparent that there could be similar users or item ratings. Just index them, so instead of looping through heavy objects, you can reduce them.
One more thing - if you are going to be running the directive the same instance, at least change the code:
var text = myTemplate.replace("{{rating}}",myItem.rating);
to a concat style string calculation:
var text = '<div>' + myItem.rating + '</div>';
This will save you a HUGE chunk on calculation. I've made a JSperf for this case, notice the difference, it's about 99% faster ;-)

dojo.query "," (or) for combining multiple selections does not work on IE 7

I am using
dojo.query('input,select',myDiv)[0].focus();
to focus the first input element found in a div container.
This will work in Firefox, but not in IE 7.
IE 7 only takes the first query into consideration:
dojo.query('input,select')[0] will select the first input element,
even if a select element is first.
dojo.query('select,input')[0] will select the first select element,
even if an input element is first.
Does anybody know a workaround for this?
If I recall correctly, dojo.query does not necessarily guarantee "chronological" order within the NodeList it returns, especially for complex queries. This is generally due to the fact that for some browsers / in some scenarios, it does have to cobble multiple disparate result sets together, and trying to reorder this based on where each element is in the document would probably be far more of a performance hit than it's worth.
That said, off the cuff I'm not sure what to suggest as an alternative. It'd be easy enough to find the first of one OR the other separately, just not while looking for both within the same query.
If your form has some kind of consistent markup around your inputs (e.g. each field is inside let's say, a div with class="field"), I suppose you could do something like this:
dojo.query('.field:first-child select, .field:first-child input')

Resources