As much as we would love to speak to that, we've unfortunately haven't done this same type of benchmarking/analysis on backbone.js. This was our first foray into a Javascript MVC-ish frameworks. After doing the paper research into AngularJS and its peers, we decided to give AngularJS a whirl by coding up this acid test and only going through the alternatives if AngularJS didn't work out.
HN user
sczerwin
When we were doing the profiling while developing the optimizations, we were surprised to find that the Javascript thread was spending something like 50% of its time in the method to remove a DOM element. Now, maybe that's because Angular is not removing DOM elements as efficiently as it could, but it was a bottleneck.
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.
We didn't need a perfectly clean solution, just one that showed we could get the performance we desired while still following Angular's main tenets -- separate model from view, leave HTML generation to templates, testability, etc. We were able to show to ourselves that we could leave all the HTML generation and updating to directives similar to ng-repeat, with just a few small optimizations. In the end, the only way we bent the abstraction layer was to rely on a non-public variable to enable us to swap in and out watchers quickly. It's a small dependency and one that could be removed through a small change to Angular's API or some other tricks folks have suggested in other comments here.
The implementation of overriding the scope's $watch method is fairly straight forward.
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.
Yes, we actually didn't modify any of the AngularJS source itself, but just overrode methods and inserted ourselves where we needed to. For example, we override $scope.$watch to intercept watch registration. However, we do rely on some of the non-public AngularJS functions to save us from duplicating a lot of code.. and that's where we push the edge.
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.
That's a very fair point and we have work to do to optimize our site for mobile viewing (I'm an engineer at Scalyr).
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.