I have a directive (), it renders a large number (>10000) of items in a table. It is used in one controller's view.
Normally, when I first visit this controller, it will take a long time to render the directive.
When I leave this controller and visit it again, it re-renders again. Is there a way to cache the rendered directive so that it can show instantly in the second visit?
just an idea, take the generated html and store it either in a service or in the local/session storage then you can check its validity using a timestamp or a data version and just reload that existing html into the container element, this would fix dependencies issues and would help you to build a more independent directive since you don't have to rely on the dom position and also this way several directive instances can render the same dataset if you use attributes for configuring your directive.
or you could render DOM in an asynchronous manner using a recursion function with a $timeout call something like
function Draw(data,index){
if(idx==data.length)
return;
//draw element idx
$timeout(function(){
Draw(data,index+1);
})
}
this way the directive will take longer but results will appear faster in the screen and you don't take the risk of blocking your browser
Is the performance hit the actual DOM rendering or some data processing that's done in the directive? If DOM manipulation is the issue, you could try hiding the content with an ng-show/ng-hide when you navigate away from the controller rather than unloading it. A good way to do this (if you're using ui-router) would be to put the directive in a parent view and have the content hidden or shown depending on which child view is active.
If it's data processing that's the issue, you can move the logic to a service. Services aren't unloaded when you change views, so it will be able to process the data once and supply it to the controller whenever is is loaded without additional processing.
Related
I am coming to you at great despair - been working on this for two days now.
We have a Single Page Application built with Angular JS. We use the $routeProvider in HTML5 mode to achieve the SPA routing. Functionality wise - all works great!
We have one global controller attached to the body element, there is one controller in the header for a quick search functionality and all other controllers are scoped to a route. There is some data shared between controllers like a currentUser object and a ViewRes object that contains string values for user's chosen language.
But, we noticed that the Chrome service takes too much RAM for our page. I used Chrome Profiles tool to see what is happening. I disabled most of our code that was using a complex directive and left out only the basics. Memory consumption was lowered a lot, but it is still obviously there. Any time I change a page, the memory increases.
In the Heap Snapshot it shows that most memory is taken by (closure) and (array). Also the Detached DOM tree is big. Please note that these snapshots are with bare elements of our application (header, footer and lightweight content). If I include our complex UI components, then memory jumps from 14MB to 50MB to 140MB... and more. Obviously we will take care for these directives, but I am concerned that our issue is global, not just to bad design of the directives.
When I open the (array) element, I notice there are bunch of them with both shallow size and retained size of 6172. Tracking the scope of that object always leads to some ng directive, like ngShow, ngIf...
As you can see from the image, the tree ends at 'cache in function()'. We use Angular 1.3.6.
EDIT: This project also includes jQuery. We were using jQuery 1.8.2 and when I switched to 1.11.2, switching between simple pages (simple ng-repeats and simple models) doesn't cause memory leaks anymore (no more detached DOM elements).
Now the complex directives are still giving me too much detached elements, so I am going to work on those now and I'll post the results here when I find out the cause.
Difficult to say what your specific problems are, but common places for memory leaks in Angular include $interval, $watches and event handlers. Each of these functions creates a closure that is not cleaned up unless you explicitly delete it upon controller tear-down.
$interval in particular is nasty, as it will continue to run until you close the browser or the Web page - even if the user moves to a different tab or application, it won't stop running!
If you create references to DOM elements within these closures, you'll soon start chewing through memory, as the references are never released and the DOM tree becomes detached as the user moves from page to page.
To resolve this, ensure you handle the $destroy event in your controller (and your directives' controllers or link functions), and explicitly clean up after any intervals, watches or event handlers have been used.
You can do this by holding a reference to each $interval, watch or event handler and simply calling it as a function in the $destroy event handler.
For example:
// eventListener to remove
var eventListener = $scope.$on('eventName', function(){…});
// remove the eventListener when the $destroy event is fired
$scope.$on('$destroy', function(){
// call the value returned from $scope.$on as a function to remove
// the event listener
eventListener();
}
// remove an event listener defined on a DOM node:
var elementEventListener = element.on('eventName', function(){…});
element.on('$destroy', function(){
elementEventListener();
}
// Stop an interval
var stop = $interval(function(){...});
$scope.$on('$destroy', function(){
stop();
}
// Finally, unbind a $watch
var watchFn = $scope.$watch('someValue', function(newVal){…}
$scope.on('$destroy', function(){
watchFn();
}
Finally, NEVER store DOM elements in the scope! (see Point #2 here for the reason why).
I have two custom element directives:
<my-directive-parent></my-directive-parent> //only one
and
<my-directive-child></my-directive-child> //variable count
The my-directive-parent directive has its own controller and templateUrl properties defined in its directive definition object. The my-directive-child directive has its own link, scope, templateUrl and require properties defined in its directive definition object. The fourth parameter passed into the link function is the parent my-directive-parent's controller. This is working as expected.
Based on user input, instances of my-directive-child are appended to or removed from the parent my-directive-parent DOM via the my-directive-parent's controller. Since I'm using ngView for the top-level page, changing pages results in Angular automatically cleaning up the various directives and their controllers of the top-level page. Upon returning, the controller for the page re-runs, but the user appended views do not show up (since they were previously wiped out automatically by Angular). I'm using a service which holds the data representing the collection of my-directive-child instances that were previously added.
My issue lies in restoring the my-directive-child directives based on this cached service data. Currently, when the my-directive-parent directive's controller is run, I'm getting the service data array that represents the instances to be added, looping through it, and adding a new my-directive-child instance to the DOM. This is working, but upon restoring each instance I need to pre-populate it with data that was previously entered by the user (of which is recorded in the service). Currently, when adding to the DOM this way each instance is in a "blank" state instead of a desired pre-populated state.
So I have the data needed to recreate the child directive instances, but I'm unable to pre-populate them with the necessary data.
I've tried to place a custom attr on the my-directive-child in an effort to forward the data to the instance's scope, but I learned from Angular's API docs that:
The new scope rule does not apply for the root of the template since the root of the template always gets a new scope
Questions:
Is my approach wrong? What should I be doing?
Is there a way to pass attrs defined on the actual element directive into the directive's scope that represents the inner template HTML?
How do I ensure a custom directive within a custom directive properly pre-populates itself?
Thank you in advance for any ideas, answers, or thoughts that might lead to a solid solution.
Part of the code in my controller deletes a DOM element:
MetrofficeApp.controller('EmployeesCtrl', function($scope) {
...
angular.element(deleteElem).remove();
$scope.$apply();
When I navigate away from this page using angular, and then come back to the same page where the deleted element is - the element is visible again.
What must I do besides $scope.$apply() to make the changes permanent (save DOM changes) between navigating pages?
I feel like you have a fundamental misunderstanding about the DOM. Every time you navigate back to a page, all code is re-invoked and templates are recreated. So, it is correct behavior that the DOM is created again.
My guess is that you have some underlying model that is visualized by the DOM. Rather than deleting components of the DOM, you should be deleting the part of the model that is visualized by the DOM (and pushing that change to the server). This way, the next time you navigate to the DOM, the model is consistent and the deleted item is no longer shown.
And a smaller point, but still important: controllers should not be manipulating the DOM directly. You should be creating directives for that.
You need to set some flag on the scope object which the angular js template can use to determine whether the show the dom element in question.
E.g.
// controller code
scope.shouldShowElement = some.flag;
// angular template
<div ng-show="shouldShowElement">...</div>
Angular templates have access to all the variables on the scope - if, at a later point, you do:
// controller code
scope.shouldShowElement = true;
scope.$apply();
Your template will update to reflect that change
I'm using the router (was using the built in one, now using ui-route but solutions for either are fine) in Angular.JS to switch between control/template pairs. When switching back and forth between a couple of these pages it takes up to a second to setup the DOM each time which looks terrible. Is there anyway of having angular keep around the DOM tree instead of recreating it each time. I guess I'd like to just hide/sow the bits for each page rather than remove/re-create them each time.
Any suggestions welcome!
You have to write your own ng-view directive to create such functionality.
The basic idea behind it is:
Before the route changes, instead of destroying the current view element, and scope, you just put it in an invisible cache DIV, and deregister scope listeners. Give the element a data attribute with the $$route.templateUrl to be able to get it back.
Then you fetch the next view from the server.
Before the route changes you check for cache item existance, and if it is in your cache div get the element from the cache, re-register listeners and put the current view the cache.
The tricky part is not to mess the $scopes up. So you might need a constructor and destructor in your $scope for the event, and maybe for the $watchers as well. I'm not sure.
But to be honest, if you using the template cache and still takes 1 second or so to render, then you might have some inefficient $watch expression, or a huge ng-repeat. You should consider some refact.
I've been researching this myself. I am working on rather old hardware in an unaccelerated browser. Initial render is a problem. My early work-around was to cache pre-compiled templates, as you are attempting. I found that this provided only a minimal speed improvement.
The real bottleneck was coming from my ng-repeat directives, the number of reflows that resulted and the number of DOM nodes/watchers I was building in each iteration.
Some sources have suggested creating a custom directive which manually assembles the dom and appends it in one go. The result is very minimal reflow and no watchers. The downside is very large. No more angular fun and a lot of unnecessary work. More importantly, none of this provided a large enough speed improvement to justify the work.
I ultimately found that the best speed improvement came from forcing hardware acceleration for each ng-repeat iteration. As suggested above, the ng-animate directive in newer versions angular make this relatively trivial.
You will see immediate page render, with minor reflow hiccups. ng-cloak does not help here. Due to the animation request, the page is not suposed to be cloaked while the repeat renders. These can, however, be rendered reasonably well with a bit of clever fun. I'm actually hiding the ng-repeat until the $location changes, showing a progress indicator in the meantime, and then toggling my ng-show. This works really nicely.
Having said all of that, precompiling your templates should be done as follows.
1) When your app starts, create a new cache for yourself. See http://docs-angularjs-org-dev.appspot.com/api/ng.$cacheFactory
2) Populate this cache with compiled templates. Inject $compile and call it on each template. Compile returns a function which you will later call against your scope. Key this function in your cache as you see fit.
3) Create a custom directive which accepts a cache key as an attribute. Inside this directive, query your compile cache for the correct compile function. Call the function against your current scope, and append the resulting DOM to the element passed into the directive.
4) Sorta win :).
If you move up to Angular 1.1.5, you can use the ng-animate attribute on you ng-view tag.
I'm not 100% sure, but I think it does some DOM caching to make the transitions work better. You could try adding adding ng-animate attribute to your tag. That might take care of it for you.
I have an angular scope containing two things:
a giant table with 10k rows that takes a second to render
some small extra information on top of it in a fixed overlay header bar
Depending on how far you have scrolled down the page/table, I have to update one of the small info bits in the header; you can think of it as a % counter how far you've scrolled.
To get the current scroll position, I have written a directive (you can also find it here):
app.directive "setWindowScroll", ->
restrict: "E"
scope:
setVariable: "="
link: (scope) ->
$(window).scroll ->
scope.$apply ->
scope.setVariable = $(window).scrollTop()
My problem is that this makes scrolling around in the table u*nsuably slow*. This is because the veriable I write to with my directive is in the extra-info in the scope, and changing that seems to cause angular to dirty-check the whole table for changes when my $apply is called.
I know that this scrolling will never change anything in my table, and would like to restrict my $apply to affect the header part of my site.
How can I make angular not dirty check the table?
Angular does the dirty checking under a process called digest. When you do a call to $scope.$digest() it will propagate to every child scope of $scope. To notify angular about changes you usually do a $scope.$apply(). At the end of the $apply() angular does a digest on the root scope, which triggers a diggest on every scope in your application. So to avoid the digest on in your scope with the big table scope, you should make sure that it is not the child of the extra information scope, and rather than doing an $scope.$apply() in your directive you could do a $scope.$digest(). This might be a bit confusing so I've made a plunker that tries to show the difference. Open your console and see the difference in the scroll event and button click:
http://plnkr.co/edit/45gKhAIt0DrozS7f0Bz2?p=preview
// this will only cause a digest in the current scope and its children
angular.element($window).bind('scroll',function(){
$scope.scrollY = $window.scrollY;
$scope.$digest();
})
// this will cause a digest in every scope
angular.element($window).bind('scroll',function(){
$scope.scrollY = $window.scrollY;
$scope.$apply();
})
When all this is said, it's a very unusual thing to do - and probably not a good idea for a number of reasons (angular doesnt scale well with thousands of elements, you can't use any of the angular event directives (ngClick etc) because they're all wrapped in $apply) - but if you can't render the table on the server side you can give this a try.
I would like to know if this is possible to do too, but -- regardless -- you might want to consider not drawing the entire table all at once.
Instead limit it (and the amount of associated data/controllers in Angular-world) to just the rows that are visible based on current scroll position + some cached rows above and below. You can still keep all the data in an array on the client, but only expose a small subset to Angular at any time.
One way to do it might be to have a small array in Angular-world which you render using ng-repeat, and then you add and remove elements in this small angular array from the big non-angular array based on scroll position.
This way Angular only knows about a small subset of your data, and should render much faster. I think this should work quite well in general because it's also much less work for the browser which doesn't need to maintain a DOM tree and rendering for 10k rows.