Cache DOM in Angular.JS - angularjs

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.

Related

If you build an AngularJS custom directive, is it bad practice to use jQuery inside the code?

Sometimes when I build an Angular custom directive, I will hear a comment as to, if it is an Angular directive, it should not use jQuery code in it, because it should be built in an AngularJS way.
And I thought it might be true, but is it possible? For example, what if the directive template has 2 sections, one is the words and one is the tiny images (such as review stars), and so you need 2 sections in your template, labeled as .description and .star-images -- so then you should need to use $.find(".description") to find that section inside your template if you need to do something to it or inside it. jqLite won't work as jqLite's find() is limited to tags only.
Another example is, what if you have a directive that doesn't have a template, but just limit the keypress to digits only, say, for an input box. So you don't want your directive to have a template as <input type="text"> but just want the user of the directive to say <input type="text" digits-input-only> and your directive is called digitsInputOnly. So in that case, don't you need to use jQuery's elem.on() or elem.bind() to listen on keypress or keydown events, and when the key down code is not a digit, then do a event.preventDefault()? So in that case, it has to use jQuery?
Or other there other ways to do it so that you really shouldn't need to use jQuery?
As a long time user of jQuery it is easy while learning angular to lean on jQuery however none of the cases mentioned are difficult to work around not having it...and in most cases are actually easier
Try removing jQuery completely from your project to avoid temptation and you will quickly realize how little you really need it
The core directives provide the majority of the event handlers needed and angular.element (jQlite) also has bind() which will accept virtually any event name. $document.bind('contextmenu', function(event) for example.
The core dom event directives all let you pass in $event for things like event.preventDefault()
<input ng-keydown="somefunc($event)">
For traverses you can always use a native method to query DOM to find an element and wrap that element (or collection) in angular.element() the same way you would with $(). The more you focus on data models and core directives first however, the less you find need to actually do dom traverses
As for plugins ... it's not a sin to use jQuery plugins in directives. There are some very commonly used angular modules that are wrappers for well known jQuery plugins ... fullcalendar and Datatables are a couple that quickly come to mind along with numerous datepickers. However often you will find situations where you may have previously leaned on a plugin to do simple tasks that angular makes easy itself and you no longer would use such plugins
The sin with using jQuery plugins is using ones that are actually easier to achieve (and test) using angular itself
In conclusion, the biggest adjustment is learning how to focus on data models first, before thinking about the DOM. Also being intimately familiar with the left side menu of the API reference where all the core directives and services are listed is a huge help

Call function after angular all directives are loaded

I have multiple custom directives in my ngApp. The demo code is:
<top-nav></top-nav>
<left-sidebar></left-sidebar>
<div class="content">
....
</div>
The height of the 'left-sidebar' needs to be adjusted according to the height of the 'top-nav'.
Similarly there are other interdependent UI tasks. I want to run the post-load code (say app.initializeUI();) only after ALL the directives have been loaded and rendered.
Now, How do I achieve that?
EDIT :
Currently I am using the following code :
App.run(function($timeout){
$timeout(function(){ app.init() },0);
});
This runs fine, however, I am not sure this is the perfect way of doing this.
EDIT 2:
For people who want to avoid setting styles in js - use CSS Flexbox. I find it much better than calculating heights post page load. I got a good understanding of flexbox here
I would create an attribute directive with isolated scope, set terminal=true and add it to your body tag. Then in your link function, setup a $watch - isInitialized which is initially false. In your $watch handler, call your app.init(), and then de-register the $watch, so that it is always initialized once.
Setting up a terminal directive has consequences - no other directive can run after the terminal directive on the same element. Make sure this is what you want. An alternative is to give your directive the lowest possible value so that it is compiled first, and linked last.
The important pieces to this solution are:
By adding your Directive to the body tag, you ensure that it is linked last.
By adding a $watch, you ensure that all other directives have gone through a digest cycle, so by the time your $watch handler is called, all other directives should have already rendered.
Note of caution: The digest cycle may run several times before scope changes stabalise. The above solution only runs after the first cycle, which may not be what you want if you really want the final rendering.

AngularJS: How to cache a rendered directive?

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.

Why does less directives/more html take longer to compile than more directives/less html?

In our app, we have several layers of nested directives. In an attempt to speed up the digest cycle, we removed some top level directives that have data-bindings to static data. Instead, we just generated the html from the static data, and compiled that. However, after doing that, we started getting unresponsive script warnings in Firefox. The compile time on this generated html was taking too long.
I'm hoping to gain a better understanding of the compile process in these 2 different scenarios so that we can optimize correctly in the future. Here is a very simplified fiddle that shows the problem. Here is the important part of it:
var repeaterHtml = '<div repeater="allData"></div>';
var staticHtml = '';
angular.forEach($scope.allData, function(currData, currIndex) {
staticHtml += '<div dir1="allData[' + currIndex + ']"></div>';
});
$element.append(staticHtml);
When using the staticHtml it takes significantly longer to compile than when just using the repeater directive. Why is this? Is it purely because there is more html to go through? As angular compiles the repeater directive does it not have to do the same compiling as I was doing manually? When the final DOM is exactly the same, what is it about the repeater directive that makes it compile so much faster?
I'd appreciate any insight into this. Thanks.
EDIT - your repeater directive does nothing special - it is just a wrapper for ng-repeat. To understand why repeater is faster than manipulating the DOM from the controller, you need to understand how ng-repeat works, what are compilation and linking phases. AFAIR Miško Hevery - has a talk about this here http://www.youtube.com/watch?v=WqmeI5fZcho
There are few things you are doing in your example that cause it to be slower than angular ng-repeat
Manipulating DOM in controller - don't do that! This practice is very bad! The reason ng-repeat is faster in this case is because it compiles the DOM once and only produces copies of the repeated element for each element in the array. In your case, you are creating element from string after the DOM has been rendered, then you modify the DOM structure by appending the element to the document, and after that, you are compiling the element you have just created against scope, which again modifies the document's DOM! You only need to compile the element once, and the append it to the parent (but do it in a directive - not in controller).
If you want to have a faster implementation of ng-repeat (but with less features - like auto updating) then write a directive that would do almost the same thing but using single template element and modify the document's DOM only once.
If you want to have better understanding how this works, I suggest reading ngRepeatDirective https://github.com/angular/angular.js/blob/master/src/ng/directive/ngRepeat.js

AngularJS performance: How to update only the scopes I know need to be updated?

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.

Resources