Hacker News new | past | comments | ask | show | jobs | submit login
Optimizing AngularJS (scalyr.com)
90 points by snewman on Nov 1, 2013 | hide | past | favorite | 41 comments



When it comes to optimising a Javascript app that involves inserting hundreds and potentially thousands of elements into a page, save yourself headaches and use documentFragment to prevent reflow and at the same time, insert a lot of elements into your page/app very quickly. I recently build an app that has reached almost 54,000 mouse click records. Each time you click, it saves that value in a database: http://coolcoolcoolcoolcool.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.


Sorry dude, I think I just broke your app with a single line of JS.. setInterval(function() {jQuery("#theimage").click()},1);


You can break any web app with a single line of javascript.


Your link gives a 502


Haha yeah, it received 12,000 clicks in the space of about 1 hour or so which took it down. Node is good, but it couldn't handle that many clicks.


Isn't that about 3 clicks a second? Was there a database issue?


It seemed the server at times would get hammered and there would be a kind of race condition happening causing a variable for the clicks I am incrementing to have a value of undefined at times (no idea). I was using nodemon instead of forever.js, so when the error would occur, it wouldn't restart. It's all good now.


On average. I suspect his clicks were bursty.


For comparison, I wondered how React would fare. Turns out it can be pretty fast. I did have to use a similar trick of breaking up the line only when the mouse is over it, but the rest is just React's architecture. See the demo here: http://jsfiddle.net/ianobermiller/tZhSp/1/

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/


In a conversation I had with someone on the React team, he said that when they designed it they were more inspired by highly performant 3D rendering engines (like those of Unreal/Quake) than they were by other web frameworks.


I timed the React example you posted. It looks like the unoptimized version is about 200ms by default and about 8-20ms after the optimization. The version of React I'm testing doesn't have some of the major perf benefits that React v0.5 has - so I suspect even the unoptimized test case with v0.5 would come in around 100ms. The optimized version would likely take around 8-15ms if I had to guess. I have a recent-ish macbook pro like the author.

edit: By the way, I really like the creative solution that the author discovered with mouse hovering.


Cool! Any reason you didn't just add a onMouseLeave handler too? If you do you don't need the componentWillReceiveProps, which I think is a bit nicer:

http://jsfiddle.net/spicyj/DGLtv/


No reason in particular, just put it together quickly. Yours is a good option since it keeps the DOM count low instead of letting it grow as you mouse around :)


Great job!

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.


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.


Very interesting, thanks for the explanation!


We did our best to follow the Angular philosophy, but we did have to bend the AngularJS abstraction layer to implement some of these optimizations. We overrode the Scope’s $watch method to intercept watcher registration, and then had to do some careful manipulation of Scope’s instance variables to control which watchers are evaluated during a $digest.

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?


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.


Now that you had the benefit of hindsight and coupled with your experience in squeezing out performance, do you have any opinion on Backbone vs Angular from a pure performance perspective?


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.


OK so you made some awesome optimized directives.. would you consider publishing them please?


We'd have to do a little cleanup work to be able to publish these, but we'll definitely do it if there's enough interest. Feel free to follow up with us at contact@scalyr.com.


I would argue there is more than sufficient interest :) All you have to do is scan the angularjs google groups for ngRepeat issues.

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.


Yup, the level of interest is becoming clear. :) We'll work on getting this published in some form. Keep an eye on our blog, or e-mail us at contact@scalyr.com and ask to be notified when the code is available.


This is a pretty good article - a lot of people don't realize that $scope.$watch is the most draining part of Angular performance-wise. It is easy to see the drain that it has by observing Batarang briefly.

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.


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.


One thing I like about Knockout, is that not every property in a model (or $scope in ng) doesn't necessarily need to be observable. I wonder if using something like Knockout for the data-binding layer could help with some of this, natively. (Not that Knockout doesn't have it's performance issues).

(Note: I'm referring to Optimization #2 & #4, specifically)


Great read - I've been doing angular based UI for errormator - check out the demo on https://errormator.com. And I've yet to optimize it, but even now the ui is rather responsive. I will probably have to adopt some of their techniques too at one point :-)


One thing I'd suggest is you stop animating the charts - clicking on one and then waiting for it to draw left to right may look pretty but is pretty annoying when I want to actually look at data!


I think you are right :-) Thank you for taking your time to check it out.


Also, I think some of these optimization concepts (reusing DOM) are probably just generic "good ideas' for almost any JavaScript framework, handling DOM creation. Some good ideas that probably can be used by many devs.


I am specially interested at how your overriding scope's $watch method.


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.


I am not sure if this will be of any help. But you could actually globally override $watch - without it feeling like a hack. Angular does provide a mechanism to do that. Since $rootScope is a service, you can have a decorator for it where you can override the $watch which will override the $watch for all scopes.

Angular Batarang overrides $watch too for instrumentation.

Consider this one more request for publishing your directives and changes :)


The only problem I see is the approach of relying on ng-mouseenter when it comes to mobile sites, as its behavior gets very weird on swipe, scroll and tap events


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.


Seems that Angular is slow by default


Well, it depends on what you're using it for. In many cases, the issues we wrestled with are fairly minor. It's only when you have a very large number of data nodes that you run into trouble.


Thanks. My statement is now confirmed.


That was my first thought too, but I'll add that Ember.js and Backbone.js are similar in that regard.


It's not. This use case is pretty niche, and generally not recommended for most apps.




Consider applying for YC's Spring batch! Applications are open till Feb 11.

Guidelines | FAQ | Lists | API | Security | Legal | Apply to YC | Contact

Search: