
Optimizing AngularJS - snewman
http://blog.scalyr.com/2013/10/31/angularjs-1200ms-to-35ms/
======
DigitalSea
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/](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.

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

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

------
wingspan
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/](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/](http://jsfiddle.net/ianobermiller/QT9Tx/1/)

~~~
spicyj
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/](http://jsfiddle.net/spicyj/DGLtv/)

~~~
wingspan
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 :)

------
candybar
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.

~~~
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.

~~~
candybar
Very interesting, thanks for the explanation!

------
anoncowherd
_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?

~~~
sczerwin
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.

~~~
sandGorgon
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?

~~~
sczerwin
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.

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

~~~
snewman
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.

~~~
pkj
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](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.

~~~
snewman
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.

------
Bahamut
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.

~~~
sczerwin
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.

------
taude
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)

------
ergo14
Great read - I've been doing angular based UI for errormator - check out the
demo on [https://errormator.com](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 :-)

~~~
youngtaff
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!

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

------
taude
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.

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

~~~
sczerwin
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.

~~~
ganarajpr
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 :)

------
felipesabino
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

~~~
sczerwin
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.

------
tipiirai
Seems that Angular is slow by default

~~~
snewman
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.

~~~
tipiirai
Thanks. My statement is now confirmed.

