Optimizing AngularJS
blog.scalyr.com
blog.scalyr.com
Inspecting the page will yield a click map where I am displaying all 53,000 mouse clicks inside of a map. As you can see I am using canvas now, but before that I was inserting them into the page and believe it or not, using documentFragment it was amazingly fast.
The reason I switched to canvas because performance on mobile was poor. 53,000+ elements rendered by Javascript on desktop didn't cause any fuss though, using documentFragment makes all the difference. Keep in mind, documentFragment really only shines when you are inserting 1000 plus elements into the page at once. One reflow against many reflows is going to cause you a lot of headaches.
Edit: updated link, forgot to save
Edit: for more info, React takes care of reusing DOM elements, and also uses event delegation by default.
Edit: for comparision, here is a version without the optimization: http://jsfiddle.net/ianobermiller/QT9Tx/1/
edit: By the way, I really like the creative solution that the author discovered with mouse hovering.
I'm not entirely sold that you needed to go that far outside vanilla AngularJS though. If you follow #3 strictly and throw out #1, I don't think you need #2 and #4, which are the ones that break the spirit of the general style of development in AngularJS. Creating elements dynamically is not a high-latency operation in modern browsers. This means in most cases, elements with a lot of watchers inside should not be hidden, but destroyed and recreated on demand, along with the scopes where the watchers are registered. As you found out, watchers are by far the most expensive part of AngularJS, because they run every digest cycle, and the solution isn't to manually manage them, but rather use the natuarl AngularJS mechanisms (ng-if, transclusion, ng-switch, and if you're on an older version of AngularJS without ng-if, ui-if) to not even render those parts until needed. ng-show in general should not be used to show and hide elements with lots of watchers inside them.
What version of AngularJS are you on? The latest should have "track by" for ng-repeat, which should largely alleviate your concerns about ng-repeat rebuilding the whole DOM when you insert new elements.
You are right though that optimization #3 is a big hammer that helps reduce the need for the other optimizations, but in the end, we found using both gives us the best result. And the other optimizations which much in terms of code.
We are using version 1.1.15 and already had AngularJS re-using the pre-existing DOM elements if it could. When we tested it (without optimization #3 turned on), we found that it still had too much lag so felt it was better to use the DOM caching optimization.
So you wanted to confirm you could use AngularJS to implement a "clean" solution, but that involved modifying/circumventing AngularJS itself. Is something wrong with this picture?
Infact, one of the solutions proposed for ngRepeat issue (with lot of caveats) has 300+ stars no github
https://github.com/Pasvaz/bindonce
Including me, there are lot of folks who would find your solution very useful. You are addressing a fundamental O(n) scaling problem in AngularJS.
I think it is possible to make most of these optimizations without bending the Angular source itself though. $scope.$watch returns an unregister function, so it should be relatively easy to unbind watches after they served their purpose.
It is an interesting idea to unregister the watcher when we do not wish it to be evaluated -- thanks for the suggestion. It would force us to recreate all of the child watchers again later, when we do need them to be evaluated again. We would have to investigate the performance implications.
We are already talking about other ways we could implement these directives by only relying on the public AngularJS calls in case there is enough interest and we want to publish the directives to the community at large. They might not be as performant, but wouldn't be broken by changes in AngularJS implementation.
(Note: I'm referring to Optimization #2 & #4, specifically)
We gave our optimization directive a fairly high priority so that it was guaranteed to be run first (among all the other directives on an element).
When the optimization directive ran, it just modified the scope variable passed to it, saving a reference to the original scope.$watch method and then setting scope.$watch to a new function we created. Inside that function, it does invoke the original scope.$watch.
We also had to override scope.$new to guarantee that any child elements, if they create new scopes, also create scopes with our override $watch method.
Angular Batarang overrides $watch too for instrumentation.
Consider this one more request for publishing your directives and changes :)
One twist on the optimization that we could have used but didn't was trigger the tokenization on mousedown and then the first token selection on mouseup. Testing with modern browsers shows that the newly visible div will receive the the mouseup event. And the average 100ms time between mouse down and mouse up gives us more than enough time for Angular to do the work of creating the tokens for one line.