Advanced boosting of angular digest loop - angularjs

I'm researching my options for boosting angular's digest loop.
I have a fairly complex app with tens of thousands active watchers at any given time.
As part of the functionality of the app I'm registering on scroll events (and handling them in an animation frame), so the digest is basically executed on each scroll, which results in an occasional drop in fps.
I've reduced the watchers count by one-time bindings and I'm now left with a few thousands watchers.
Currently I'm attempting to write a directive to suspend all watchers of out of viewport elements.
So I started playing with angular internals and came up with the following directive:
app.directive('ngSuspendable', function () {
return {
restrict: 'A',
scope: true,
link: function (scope, element, attr) {
var _watchersMap = {};
var _suspended = true;
suspendWatchersTree();
function suspendWatchersTreeRecursive(currentScope, includeSiblings) {
while (currentScope != null) {
if ((currentScope.$$watchers != null) &&
(currentScope.$$watchers.length > 0) &&
(typeof _watchersMap[currentScope.$id] == "undefined")) {
_watchersMap[currentScope.$id] = currentScope.$$watchers;
currentScope.$$watchers = [];
}
if (currentScope.$$childHead != null) {
suspendWatchersTreeRecursive(currentScope.$$childHead, true);
}
if (includeSiblings) {
currentScope = currentScope.$$nextSibling;
}
else {
currentScope = null;
}
}
}
function unsuspendWatchersTreeRecursive(currentScope, includeSiblings) {
while (currentScope != null) {
if ((typeof _watchersMap[currentScope.$id] != "undefined") &&
(_watchersMap[currentScope.$id].length > 0)) {
if ((currentScope.$$watchers != null) &&
(currentScope.$$watchers.length > 0)) {
currentScope.$$watchers = currentScope.$$watchers.concat(_watchersMap[currentScope.$id]);
}
else {
currentScope.$$watchers = _watchersMap[currentScope.$id];
}
}
if (currentScope.$$childHead != null) {
unsuspendWatchersTreeRecursive(currentScope.$$childHead, true);
}
if (includeSiblings) {
currentScope = currentScope.$$nextSibling;
}
else {
currentScope = null;
}
}
}
function suspendWatchersTree() {
suspendWatchersTreeRecursive(scope, false);
}
function unsuspendWatchersTree() {
unsuspendWatchersTreeRecursive(scope, false);
}
scope.inView = function(evnt, model, htmlElementId, triggeringEvent, isInView, inViewPart) {
if (!isInView) {
suspendWatchersTree();
_suspended = true;
}
if ((isInView) && (_suspended)) {
unsuspendWatchersTree();
_watchersMap = {};
_suspended = false;
}
}
}
}
});
Upon initialization this directive removes all watchers from current scope and all child scopes (not sure if it captures also isolated scopes).
Then, when the element is in view it adds back the watchers, and removes them when out of view.
I know that it doesn't necessarily captures all watchers, as some watchers might be added post linking, but that seems negligible, and they will be removed once the element comes in view and out of view again. If I could somehow hook to the watchers addition and add them to the map when suspsended that could be nice but I guess its not a must.
This seems to be working well but I'm not sure what are the caveats for such approach. I'm uncomfortable with playing around with angular internals and messing up things and reaching unstable conditions.
Any ideas and remarks will be greatly appreciated.

I've updated the above code with the most recent changes.
Further testing showed it works fairly well, some watches might not be suspended at initialization but that's a price I can live with. It greatly enhanced my digest loop and boosted the app performance by removing unrequired watches for out of viewport elements.

Related

How to trigger $destroy event of already removed element scope?

Consider the following directive:
class HasPermissionDirective {
constructor(PermissionService) {
this.restrict = 'A';
this.priority = 1005;
this.PermissionService = PermissionService;
}
compile(element, attrs) {
let permission = _.trim(attrs.hasPermission),
hide = _.get(attrs, 'hideOnly'),
$element = angular.element(element);
if (permission && !this.PermissionService.hasPermission(permission)) {
if (!_.isUndefined(hide)) {
hide = _.trim(hide);
if (hide === 'visibility') {
$element.css('visibility', 'hidden');
} else {
$element.hide();
}
} else {
$element.remove();
}
}
}
link($scope, $element, attrs, ctrl) {
$scope.$destroy();
}
}
HasPermissionDirective.$inject = ['PermissionService'];
The problem is now, the $scope.$destroy() is executed always, for every element-scope the directive is attached to (of course).
When I'm now add a "isRemoved" member variable and set it to true in case the element was removed and do the following in the link function:
if (this.isRemoved) {
$scope.$destroy();
}
Of course the $scope.$destroy() is triggered for every element-scope as soon as at least one element is removed, cause the directive is handled as a singleton not as an instance.
I cannot add any information to the element node since it seems to be removed after compile and is only a comment node "ngInclude: undefined" (no, I don't remove the node, I add an data-attribute and want to get it inside of the link function: $element.data('remove', true) and then want to $destroy and remove()). EDIT: This seems to be the transclude-behavior of the ngInclude-directive.
If I remove the $scope.$destroy() from the link-function and just remove the node, the ngInclude-directive is still running ...
Waht I want? I just want to remove the element from DOM while compile time since the current user has not the permission to see this element/directive/view and I also want to avoid further processing of directives (in my case the ng-include which should not unnecessary request templates (since our server will respond 401 anyway) and so on).
UPDATE: I guess i need a way to set the terminal option inside of the compile function to stop the processing of the upcoming directives. My problem is that ngInclude is running even though the element was removed before.
Solution found! I also have to to use transclusion (it was clear after I was checking the impl of ngIf) and its only possible inside of (pre/post)link function(s), so here the impl. for someone who runs into similar problems:
class HasPermissionDirective {
constructor(PermissionService) {
this.restrict = 'A';
this.priority = 1011; // high prio so the element is removed before all other directives can be processed
this.transclude = 'element';
this.$$tlb = true; // BAD! But w/o, transclusion for different directives won't work :(
this.PermissionService = PermissionService;
}
preLink($scope, $element, attrs, ctrl, $transclude) {
let $newScope = $scope.$new(),
hide = _.get(attrs, 'hideOnly');
$transclude($newScope, ($clone) => {
if (!this.PermissionService.hasPermission(_.trim(attrs.hasPermission))) {
if (!_.isUndefined(hide)) {
hide = _.trim(hide);
if (hide === 'visibility') {
$clone.css('visibility', 'hidden');
} else {
$clone.hide();
}
} else {
$newScope.$destroy();
$newScope = null;
$clone.remove();
$clone = null;
}
} else {
// in case the user has the permission we have to attach the element to the DOM (cause of transclusion)
$element.after($clone);
}
});
}
}
HasPermissionDirective.$inject = ['PermissionService'];
I did also outsource the implementation to a controller so I can reuse the logic, but I wan't to provide a complete example for clarification :)

CheckAll/UncheckAll issue with Subscribe ? Knockout

I been trying to do checkbox Checkall and UnCheckall using subscribe and i'm partially successful doing that but i am unable to find a fix in couple of scenarios when i am dealing with subscribe .
Using subscribe :
I am here able to checkAll uncheckAll but when i uncheck a child checkbox i.e test1 or test2 i need my parent checkbox name also to be unchecked and in next turn if i check test1 the parent checkbox should be checked i.e keeping condition both child checkboxes are checked .
For fiddle : Click Here
ViewModel :
self.selectedAllBox.subscribe(function (newValue) {
if (newValue == true) {
ko.utils.arrayForEach(self.People(), function (item) {
item.sel(true);
});
} else {
ko.utils.arrayForEach(self.People(), function (item) {
item.sel(false);
});
}
});
The same scenario can be done perfectly in easy way using computed but due some performance issues i need to use subscribe which is best way it wont fire like computed onload .
Reference : Using computed same thing is done perfectly check this Fiddle
I tried to use change event in individual checkbox binding but its a dead end till now.
Any help is appreciated .
Your subscription only applies to edits on the selectedAllBox. To do what you want, you'll need subscriptions on every Person checkbox as well, to check for the right conditions and uncheck the selectedAllBox in the right situations there.
It strikes me as odd that this would be acceptable but using computed() is not. Maybe you should reconsider that part of your answer. I would much rather compute a "isAllSelected" value based on my viewModel state, then bind the selectedAllBox to that.
I solved a similar problem in my own application a couple of years ago using manual subscriptions. Although the computed observable method is concise and easy to understand, it suffers from poor performance when there's a large number of items. Hopefully the code below speaks for itself:
function unsetCount(array, propName) {
// When an item is added to the array, set up a manual subscription
function addItem(item) {
var previousValue = !!item[propName]();
item[propName]._unsetSubscription = item[propName].subscribe(function (latestValue) {
latestValue = !!latestValue;
if (latestValue !== previousValue) {
previousValue = latestValue;
unsetCount(unsetCount() + (latestValue ? -1 : 1));
}
});
return previousValue;
}
// When an item is removed from the array, dispose the subscription
function removeItem(item) {
item[propName]._unsetSubscription.dispose();
return !!item[propName]();
}
// Initialize
var tempUnsetCount = 0;
ko.utils.arrayForEach(array(), function (item) {
if (!addItem(item)) {
tempUnsetCount++;
}
});
var unsetCount = ko.observable(tempUnsetCount);
// Subscribe to array changes
array.subscribe(function (changes) {
var tempUnsetCount = unsetCount();
ko.utils.arrayForEach(changes, function (change) {
if (change.moved === undefined) {
if (change.status === 'added') {
if (!addItem(change.value))
tempUnsetCount++;
} else {
if (!removeItem(change.value))
tempUnsetCount--;
}
}
});
unsetCount(tempUnsetCount);
}, null, 'arrayChange');
return unsetCount;
}
You'll still use a computed observable in your viewmodel for the the select-all value, but now it'll only need to check the unselected count:
self.unselectedPeopleCount = unsetCount(self.People, 'Selected');
self.SelectAll = ko.pureComputed({
read: function() {
return self.People().length && self.unselectedPeopleCount() === 0;
},
write: function(value) {
ko.utils.arrayForEach(self.People(), function (person) {
person.Selected(value);
});
}
}).extend({rateLimit:0});
Example: http://jsfiddle.net/mbest/dwnv81j0/
The computed approach is the right way to do this. You can improve some performance issues by using pureComputed and by using rateLimit. Both require more recent versions of Knockout than the 2.2.1 used in your example (3.2 and 3.1, respectively).
self.SelectAll = ko.pureComputed({
read: function() {
var item = ko.utils.arrayFirst(self.People(), function(item) {
return !item.Selected();
});
return item == null;
},
write: function(value) {
ko.utils.arrayForEach(self.People(), function (person) {
person.Selected(value);
});
}
}).extend({rateLimit:1});
http://jsfiddle.net/mbest/AneL9/98/

Force angular to consider scope property "dirty"

Is there a way to force angular to re-render things bound to a property, even though the property has not changed value? e.g:
$scope.size = 1;
<div some-prop="size"></div>
$scope.$needsRender('size') // psuedocode
$scope.$apply(); // re-renders the <div>
Unfortunately I can't manage the property entirely in angular for performance reasons, which is why I need this "reset".
Angular DOM beeing manipulated outside of angular is ugly. Well to say the truth, it's more than that. People doing this probably deserves to die. slowly. Painfully.
But anyway yes, it is possible.
Short answer : You can do this be forcing the execution of a scope's watcher to fire.
module.factory("scopeUtils", function($parse) {
var scopeUtils = {
/**
* Apply watchers of given scope even if a digest progress is already in process on another level.
* This will only do a one-time cycle of watchers, without cascade digest.
*
* Please note that this is (almost) a hack, behaviour may be hazardous so please use with caution.
*
* #param {Scope} scope : scope to apply watchers from.
*/
applyWatchers : function(scope) {
scopeUtils.traverseScopeTree(scope, function(scope) {
var watchers = scope.$$watchers;
if(!watchers) {
return;
}
var watcher;
for(var i=0; i<watchers.length; i++) {
watcher = watchers[i];
var value = watcher.get(scope);
watcher.fn(value, value, scope);
}
});
},
traverseScopeTree : function(parentScope, traverseFn) {
var next,
current = parentScope,
target = parentScope;
do {
traverseFn(current);
if (!(next = (current.$$childHead ||
(current !== target && current.$$nextSibling)))) {
while(current !== target && !(next = current.$$nextSibling)) {
current = current.$parent;
}
}
} while((current = next));
}
};
return scopeUtils;
});
Use it simply like that :
scopeUtils.applyWatchers(myScope);

Testing AngularJS with Selenium

I have a SPA application on stack ASP MVC + AngularJS and I'd like to test the UI.
For now I'm trying Selenium with PhantomJS and WebKit drivers.
This is a sample testing page - view with single element. The list items <li> load dynamically from server and are bounded by Angular.
<div id="items">
<li>text</li>
<li>text2</li>
</div>
I'm trying to pass a test and there is an error in this line:
_driver.FindElements(By.TagName('li'))
At this point there are no loaded elements and _driver.PageSource doesn't contain elements.
How can I wait for the items to load? Please do not suggest Thread.Sleep()
This will wait for page loads / jquery.ajax (if present) and $http calls, and any accompanying digest/render cycle, throw it in a utility function and wait away.
/* C# Example
var pageLoadWait = new WebDriverWait(WebDriver, TimeSpan.FromSeconds(timeout));
pageLoadWait.Until<bool>(
(driver) =>
{
return (bool)JS.ExecuteScript(
#"*/
try {
if (document.readyState !== 'complete') {
return false; // Page not loaded yet
}
if (window.jQuery) {
if (window.jQuery.active) {
return false;
} else if (window.jQuery.ajax && window.jQuery.ajax.active) {
return false;
}
}
if (window.angular) {
if (!window.qa) {
// Used to track the render cycle finish after loading is complete
window.qa = {
doneRendering: false
};
}
// Get the angular injector for this app (change element if necessary)
var injector = window.angular.element('body').injector();
// Store providers to use for these checks
var $rootScope = injector.get('$rootScope');
var $http = injector.get('$http');
var $timeout = injector.get('$timeout');
// Check if digest
if ($rootScope.$$phase === '$apply' || $rootScope.$$phase === '$digest' || $http.pendingRequests.length !== 0) {
window.qa.doneRendering = false;
return false; // Angular digesting or loading data
}
if (!window.qa.doneRendering) {
// Set timeout to mark angular rendering as finished
$timeout(function() {
window.qa.doneRendering = true;
}, 0);
return false;
}
}
return true;
} catch (ex) {
return false;
}
/*");
});*/
Create a new class that lets you figure out whether your website using AngularJS has finished making AJAX calls, as follows:
import org.openqa.selenium.JavascriptExecutor;
import org.openqa.selenium.WebDriver;
import org.openqa.selenium.support.ui.ExpectedCondition;
public class AdditionalConditions {
public static ExpectedCondition<Boolean> angularHasFinishedProcessing() {
return new ExpectedCondition<Boolean>() {
#Override
public Boolean apply(WebDriver driver) {
return Boolean.valueOf(((JavascriptExecutor) driver).executeScript("return (window.angular !== undefined) && (angular.element(document).injector() !== undefined) && (angular.element(document).injector().get('$http').pendingRequests.length === 0)").toString());
}
};
}
}
You can use it anywhere in the your code by using the following code:
WebDriverWait wait = new WebDriverWait(getDriver(), 15, 100);
wait.until(AdditionalConditions.angularHasFinishedProcessing()));
We have had a similar issue where our in house framework is being used to test multiple sites, some of these are using JQuery and some are using AngularJS (and 1 even has a mixture!). Our framework is written in C# so it was important that any JScript being executed was done in minimal chunks (for debugging purposes). It actually took a lot of the above answers and mashed them together (so credit where credit is due #npjohns). Below is an explanation of what we did:
The following returns a true / false if the HTML DOM has loaded:
public bool DomHasLoaded(IJavaScriptExecutor jsExecutor, int timeout = 5)
{
var hasThePageLoaded = jsExecutor.ExecuteScript("return document.readyState");
while (hasThePageLoaded == null || ((string)hasThePageLoaded != "complete" && timeout > 0))
{
Thread.Sleep(100);
timeout--;
hasThePageLoaded = jsExecutor.ExecuteScript("return document.readyState");
if (timeout != 0) continue;
Console.WriteLine("The page has not loaded successfully in the time provided.");
return false;
}
return true;
}
Then we check whether JQuery is being used:
public bool IsJqueryBeingUsed(IJavaScriptExecutor jsExecutor)
{
var isTheSiteUsingJQuery = jsExecutor.ExecuteScript("return window.jQuery != undefined");
return (bool)isTheSiteUsingJQuery;
}
If JQuery is being used we then check that it's loaded:
public bool JqueryHasLoaded(IJavaScriptExecutor jsExecutor, int timeout = 5)
{
var hasTheJQueryLoaded = jsExecutor.ExecuteScript("jQuery.active === 0");
while (hasTheJQueryLoaded == null || (!(bool) hasTheJQueryLoaded && timeout > 0))
{
Thread.Sleep(100);
timeout--;
hasTheJQueryLoaded = jsExecutor.ExecuteScript("jQuery.active === 0");
if (timeout != 0) continue;
Console.WriteLine(
"JQuery is being used by the site but has failed to successfully load.");
return false;
}
return (bool) hasTheJQueryLoaded;
}
We then do the same for AngularJS:
public bool AngularIsBeingUsed(IJavaScriptExecutor jsExecutor)
{
string UsingAngular = #"if (window.angular){
return true;
}";
var isTheSiteUsingAngular = jsExecutor.ExecuteScript(UsingAngular);
return (bool) isTheSiteUsingAngular;
}
If it is being used then we check that it has loaded:
public bool AngularHasLoaded(IJavaScriptExecutor jsExecutor, int timeout = 5)
{
string HasAngularLoaded =
#"return (window.angular !== undefined) && (angular.element(document.body).injector() !== undefined) && (angular.element(document.body).injector().get('$http').pendingRequests.length === 0)";
var hasTheAngularLoaded = jsExecutor.ExecuteScript(HasAngularLoaded);
while (hasTheAngularLoaded == null || (!(bool)hasTheAngularLoaded && timeout > 0))
{
Thread.Sleep(100);
timeout--;
hasTheAngularLoaded = jsExecutor.ExecuteScript(HasAngularLoaded);
if (timeout != 0) continue;
Console.WriteLine(
"Angular is being used by the site but has failed to successfully load.");
return false;
}
return (bool)hasTheAngularLoaded;
}
After we check that the DOM has successfully loaded, you can then use these bool values to do custom waits:
var jquery = !IsJqueryBeingUsed(javascript) || wait.Until(x => JQueryHasLoaded(javascript));
var angular = !AngularIsBeingUsed(javascript) || wait.Until(x => AngularHasLoaded(javascript));
If you're using AngularJS then using Protractor is a good idea.
If you use protractor you can use it's waitForAngular() method which will wait for http requests to complete. It's still good practise to wait for elements to be displayed before acting on them, depending on your language and implementation it might look this in a synchronous language
WebDriverWait wait = new WebDriverWait(webDriver, timeoutInSeconds);
wait.until(ExpectedConditions.visibilityOfElementLocated(By.id<locator>));
Or in JS you can use wait method which executes a function until it returns true
browser.wait(function () {
return browser.driver.isElementPresent(elementToFind);
});
You may just mine protractor for useful code snippets. This function blocks until Angular is done rendering the page. It is a variant of Shahzaib Salim's answer, except that he is polling for it and I am setting a callback.
def wait_for_angular(self, selenium):
self.selenium.set_script_timeout(10)
self.selenium.execute_async_script("""
callback = arguments[arguments.length - 1];
angular.element('html').injector().get('$browser').notifyWhenNoOutstandingRequests(callback);""")
Replace 'html' for whatever element is your ng-app.
It comes from https://github.com/angular/protractor/blob/71532f055c720b533fbf9dab2b3100b657966da6/lib/clientsidescripts.js#L51
I did the following code and it helped me for the async race condition failures.
$window._docReady = function () {
var phase = $scope.$root.$$phase;
return $http.pendingRequests.length === 0 && phase !== '$apply' && phase !== '$digest';
}
Now in selenium PageObject model, you can wait for
Object result = ((RemoteWebDriver) driver).executeScript("return _docReady();");
return result == null ? false : (Boolean) result;
If your web app is indeed created with Angular as you say, the best way to do end-to-end testing is with Protractor.
Internally, Protractor uses its own waitForAngular method, to ensure Protractor waits automatically until Angular has finished modifying the DOM.
Thus, in the normal case, you would never need to write an explicit wait in your test cases: Protractor does that for you.
You can look at the Angular Phonecat tutorial to learn how to set up Protractor.
If you want to use Protractor seriously, you will want to adopt pageobjects. If you want an example of that have a look at my page object test suite for the Angular Phonecat.
With Protractor you write your tests in Javascript (Protractor is indeed based on Node), and not in C# -- but in return Protractor handles all waiting for you.
For my particular problem with the HTML page containing iframes and developed with AnglularJS the following trick saved me a lot of time:
In the DOM I clearly saw that there is an iframe which wraps all the content.
So following code supposed to work:
driver.switchTo().frame(0);
waitUntilVisibleByXPath("//h2[contains(text(), 'Creative chooser')]");
But it was not working and told me something like "Cannot switch to frame. Window was closed".
Then I modified the code to:
driver.switchTo().defaultContent();
driver.switchTo().frame(0);
waitUntilVisibleByXPath("//h2[contains(text(), 'Creative chooser')]");
After this everything went smoothly.
So evidently Angular was mangling something with iframes and just after loading the page when you expect that driver is focused on default content it was focused by some already removed by Angular frame.
Hope this may help some of you.
If you don't want to make the entire switch to Protractor but you do want to wait for Angular I recommend using Paul Hammants ngWebDriver (Java). It's based on protractor but you don't have to make the switch.
I fixed the problem by writing an actions class in which I waited for Angular (using ngWebDriver's waitForAngularRequestsToFinish()) before carrying out the actions (click, fill, check etc.).
For a code snippet see my answer to this question
Here is an example for how to wait on Angular if you're using WebDriverJS. Originally I thought you had to create a custom condition, but wait accepts any function.
// Wait for Angular to Finish
function angularReady(): any {
return $browser.executeScript("return (window.angular !== undefined) && (angular.element(document).injector() !== undefined) && (angular.element(document).injector().get('$http').pendingRequests.length === 0)")
.then(function(angularIsReady) {
return angularIsReady === true;
});
}
$browser.wait(angularReady, 5000).then(...);
Sadly this doesn't work with PhantomJS because of CSP (content-security-policy) and unsafe-eval. Can't wait for headless Chrome 59 on Windows.
I have implemented usage based on D Sayar's answer
And it might helpful for someone. You just have to copy all boolean functions mention over there in to single class, And then add below PageCallingUtility() method. This method is calling internal dependency.
In your normal usage you need to directly call PageCallingUtility() method.
public void PageCallingUtility()
{
if (DomHasLoaded() == true)
{
if (IsJqueryBeingUsed() == true)
{
JqueryHasLoaded();
}
if (AngularIsBeingUsed() == true)
{
AngularHasLoaded();
}
}
}
Beside eddiec's suggest. If you test an AngularJS app, I strongly suggest you to think about protractor
Protractor will help you solve the waiting matter (sync, async). However, there are some notes
1 - You need to develop your test in javascript
2 - There are some different mechanism in handling flow

Is there a way I can speed up the way my grid reacts when I have many rows?

I have code that creates a grid like this:
<div ng-repeat="row in home.grid.data track by row.examId">
<div>{{ row.examId }}</div>
<div>xxxx</div>
</div>
I have more columns after these.
Is there a way I can speed up the way my page reacts? It seems that when
I have a lot of data in the grid then the pages reacts slowly. Would it
make a difference if I used ng-model in an input type field for the row.examId. Note that
some of the fields that follow can be edited but most are just display only.
I believe bindonce does exactly what you need.
By reducing the number of watchers it allows the page to become more responsive. Check their demos.
This is what I have done. There are two ways. Irrespective of both solutions, use bindOnce. Keep a look out on the number of watchers on the page. Look at the end of this solution - how to keep track of watchers on a page.
I have added a solution 3 and this is working awesome, styling is a bit difficult
Solution 1:
Use a pagination control with bind once.
Solution 2
This is what worked for me and it is very elegant. You repeat with bindonce and then implement infinite scrolling. I have followed this blog post and it works like a charm. The idea is you limit the number of rows and change the limit as you scroll.
ng-repeat="item in items | orderBy:prop | filter:query | limitTo:limit"
Essentially, your html would look like this. I have modified the OP's code to use bindonce.
<div id="estates-listing" extend-height>
<div class="content" infinite-scroll="addMoreItems()" infinite-scroll-distance="2">
<div class="content-wrapper">
<div class="house" bindonce="estate" ng-animate="'animate'" ng-class="{inactive: (selectedEstate != null || selectedEstate != undefined) && estate.id!=selectedEstate.id , active:(selectedEstate != null || selectedEstate != undefined) && estate.id==selectedEstate.id}" ng-repeat="estate in estates | orderBy: orderProp : orderReverse | limitTo: config.itemsDisplayedInList track by estate.id" ng-mouseover="highlightMarker(estate)" ng-mouseleave="leaveMarker(estate)" ng-click="showDetailview(estate.id)" >
<div id="l-el{{estate.id}}">
</div>
</div>
</div>
</div>
</div>
Here is the infinite scroll directive from the post. Add this to your app, please don't use the standard infinite scroll using bower install.
app.directive('infiniteScroll', [
'$rootScope', '$window', '$timeout', function($rootScope, $window, $timeout) {
return {
link: function(scope, elem, attrs) {
var checkWhenEnabled, handler, scrollDistance, scrollEnabled;
$window = angular.element($window);
elem.css('overflow-y', 'scroll');
elem.css('overflow-x', 'hidden');
elem.css('height', 'inherit');
scrollDistance = 0;
if (attrs.infiniteScrollDistance != null) {
scope.$watch(attrs.infiniteScrollDistance, function(value) {
return scrollDistance = parseInt(value, 10);
});
}
scrollEnabled = true;
checkWhenEnabled = false;
if (attrs.infiniteScrollDisabled != null) {
scope.$watch(attrs.infiniteScrollDisabled, function(value) {
scrollEnabled = !value;
if (scrollEnabled && checkWhenEnabled) {
checkWhenEnabled = false;
return handler();
}
});
}
$rootScope.$on('refreshStart', function(event, parameters){
elem.animate({ scrollTop: "0" });
});
handler = function() {
var container, elementBottom, remaining, shouldScroll, containerBottom;
container = $(elem.children()[0]);
elementBottom = elem.offset().top + elem.height();
containerBottom = container.offset().top + container.height();
remaining = containerBottom - elementBottom ;
shouldScroll = remaining <= elem.height() * scrollDistance;
if (shouldScroll && scrollEnabled) {
if ($rootScope.$$phase) {
return scope.$eval(attrs.infiniteScroll);
} else {
return scope.$apply(attrs.infiniteScroll);
}
} else if (shouldScroll) {
return checkWhenEnabled = true;
}
};
elem.on('scroll', handler);
scope.$on('$destroy', function() {
return $window.off('scroll', handler);
});
return $timeout((function() {
if (attrs.infiniteScrollImmediateCheck) {
if (scope.$eval(attrs.infiniteScrollImmediateCheck)) {
return handler();
}
} else {
return handler();
}
}), 0);
}
};
}
]);
Solution 3:
Be adventurous and use UI-Grid, UI Grid is the new ng-grid. It is not production ready, but we are playing around in production in a table where we have over 1000 records- out of the box it is awesome. The tutorials are extensive but not much SO support. It has virtualization in built and since it is an extension of ng-grid, it has a lot of backward compatibility. Here is a example with 10,000 rows
Number of watchers on the page:
Here is a function to track the number of watchers on the page. The thumb rule is never exceed 2500 watchers, but we restrict ourselves to < 1000.
$scope.TotalWatchers = function () {
var root = $(document.getElementsByTagName('body'));
var watchers = 0;
var f = function (element) {
if (element.data().hasOwnProperty('$scope')) {
watchers += (element.data().$scope.$$watchers || []).length;
}
angular.forEach(element.children(), function (childElement) {
f(angular.element(childElement));
});
};
f(root);
return watchers;
};
The biggest thing I've found to help with performance of large tables is to limit event binding to the parent object and make use of bubbling to capture the events of the children.
In the event of the parent you can get which target was hit. I use the following code.
obj.onclick = function (e) {
e = window.event || e;
var t = e.target || e.srcElement;
}
in this event e is your regular event object and t is the object that was the initial target before the event bubbled. You need to use t as 'this' references the object that the event is bound to not the object that triggered the event.
Where I was using the code which was a really large table it reduced the rendering time of the table by almost 80% by moving the events to a parent node that was static. This also helps if you need to update the contents as you don't have to re-bind any events.
Hope this helps.

Resources