6

In the discussion below this article there's a comment by Renan Cakirerk to the effect that, according to an Angular developer, Angular UI performance might degrade beyond 2000 data-bound objects.

It made me seriously consider whether pursuing my non-trivial app with Angular is a good idea. A good app is a fast app after all. I don't want to invest months building something to be bitten at the end.

I am interested in hearing from Angular non-trivial app builders about

  • general strategies people have successfully used for dealing with performance degradation
  • specific strategies based on my requirements and design pattern (below)
  • whether I should abandon Angular altogether at this early stage in the project to avoid a looming "grind to halt"

It's too much of a risk to wait for the possible "ES6 power and perf boons like Object.observe" and future versions that will might developers more fine-grained control on the $apply / $digest cycle so that $scope-limited dirty-checking can be triggered" (Brian Frichette mentions these in the same discussion). I want to know that complex apps can be fast today on v1.2.15.

More details about my problem/solution...

I'm building an app with very rich functionality, where each object (eg user) has many functions that can be done to it, eg linking them to other users, changing their properties, sending them messages, etc.

The spec has upwards of 20 functions on this object: droppable zones, context sensitive toolbar icons (eg the way Word has mini-toolbars that appear near the mouse when you select some text).

These options need to hide and show based on certain mouse actions, like hovering and dragging, and depend on the state of the particular user object (many icons and drop options will show in some circumstances and not others)

Now, the way I've started building this is to have each individual icon and drop area, drag handle, etc as a separate data-bound element with an ng-show (or similar) that's keyed into our custom business logic.

Eg

<user>
  <menuicon1 ng-show="business-logic1"/>
  <menuicon2 ng-show="business-logic2"/>
  <dropzone1 ng-show="business-logic3"/>
  <draghandle ng-show="business-logic4"/>
  <changessavedicon ng-show="business-logic5"/>
  .....
</user>

Assuming the 2000 theoretical limit above is to be feared, then 20 custom showable hideable bits means 100 users (shown using the amazing ng-repeat) is my limit! Maybe showing 100 is silly and I can attack this with filtering etc, but it seems to me that dividing by 20 drastically reduces my object "bandwidth". And what happens when the boss wishes to add 10 more functions?

If I were doing this the jQuery way, I'd probably construct and destroy many of the icons and menu items as needed. Slightly less responsive per hover/drag, but at least the app can scale the number of objects that way.

poshest
  • 4,157
  • 2
  • 26
  • 37
  • I'm suggesting the following based on what you said you would do if you were doing it the 'jQuery' way. The ng-if directive can be used instead of ng-show, so that things are not in the DOM if they aren't being shown - sorry that I can't be of more help – link64 Mar 29 '14 at 01:54
  • 1
    Also, read this question for more details: http://stackoverflow.com/questions/9682092/databinding-in-angularjs/9693933#9693933 – link64 Mar 29 '14 at 02:02
  • Great link, thanks link64! MW's comment there is very relevant :) – poshest Mar 29 '14 at 17:03

4 Answers4

4

I have encountered the somewhat infamous ng-repeat performance issue. I have a table with about 10 columns which consists of a row for each day. If I try:

<tr ng-repeat="row in rows">
  <td ng-repeat="column in columns">
    {{ row[column.id] }}
  </td>
</tr>

I run into performance issues after creating on the order of 100-200 days.

But the truth is, there are a ton of stupid things about this approach. For one, no display can show this many rows. Why add a bunch of crap to the screen that isn't going to be rendered? Also, as others have made apparent, it's unlikely that a user can meaningfully interact with enough items on a page to warrant 2k bindings.

I was going to go with some complicated pre-rendering of rows or even the whole table that would compile all of the html and then update every time there was a change to the table data. But then I had some little widgets in the rows that I wanted to have bindings on.

So instead, I went with http://binarymuse.github.io/ngInfiniteScroll/ It makes it super easy to add or remove items on the fly from your page. Now I can show 40ish rows at any time, and when the user scrolls in either direction it will append the new rows and get rid of the old. I had to do some modifications to implement the remove elements part, but for me it's by far the best option because it allows me to stick with the angularjs mentality which I really like and still get good performance.

It seems to me that your issue can be fairly trivially solved with an ng-if instead of ng-show. If you remove the elements from the DOM, there shouldn't be any more issue with bindings for that item.

Should be easy enough to test with what you have and some extra ng-repeats if necessary though right? It's super easy to plug in infinite-scroll to give it a whirl.

urban_raccoons
  • 3,499
  • 1
  • 22
  • 33
  • Thanks! A few responses which I'll separate into different comments (a) I'll have a play with `ngInfiniteScroll`, but are there any examples of your approach? Only the basic `ngInfiniteScroll` example is working right now and it doesn't do any tear down of already viewed items... – poshest Mar 29 '14 at 13:15
  • (b) Thanks for the steer on `ngIf`! :) Sounds like a similar effect to the jQuery approach I mentioned. But aren't watches still deployed on every `ngIf`, and isn't that the concern? Or is Angular smart enough to ignore elements nested _inside_ the `ngIf`ed element so that they aren't "data-bound" _until_ the `ng-if` evaluates `true`? Eg in `...` is anything _inside_ `` not "data-bound" until it's hovered over? If so then I believe that will address my concern. – poshest Mar 29 '14 at 13:28
  • (c) "it's unlikely that a user can meaningfully interact with enough items on a page to warrant 2k bindings": no, true, my concern was all the functionality that's _hidden_ from the user, until it springs into life upon hover/drag/click/etc. Eg Gmail chat bar: Hover over person's name ==> box pops up with bigger photo, video/chat icons and "Add to circles"; hover over "Add to circles" ==> list of circles, and so on. As I said in (b), if, using `ng-if` all the detail inside the "pop up" isn't being "watched" until it's hovered, then problem solved. – poshest Mar 29 '14 at 14:22
  • to self-answer (b) and (c), it looks like `ngIf` (and `ngSwitch` too) do exactly what I was hoping. See http://youtu.be/zyYpHIOrk_Y?t=10m30s – poshest Mar 29 '14 at 17:56
  • I also love the look of the "fast bind on notify" starting here http://youtu.be/zyYpHIOrk_Y?t=12m30s – poshest Mar 29 '14 at 18:08
  • Glad that ngIf looks like it will work for you! W/ regards my implementation of ngInfiniteScroll, I haven't published anything because I work for a group that has a code review policy before anything can be open sourced. I can say that if you're reasonably familiar with angular and JS that it's not too difficult, but that the real hard part is the way that window scroll handling takes place. – urban_raccoons Apr 01 '14 at 00:48
  • With infinite scroll, you simply append more items to the bottom of the page. When you're removing items from the top at the same time, the page is no longer getting longer and so the window is just sitting at the bottom of the page (which is the condition upon which ngInfiniteScroll loads more items. That means that you have to hack in some sort of fix so that the window isn't stuck to the bottom. You can use a $(window).scrollTop() or something similar. It's not perfect though. – urban_raccoons Apr 01 '14 at 00:48
  • You could also use a hack which just creates blank elements in order to take up enough space to extend the window. Then you have to do a similar watch for when the window moves back up and add more elements to the top and remove them from the bottom. – urban_raccoons Apr 01 '14 at 00:49
  • I thought of your "blank elements" idea as well. It seems like the best of a bad bunch, but I'm scared that the element re-paint upon scrolling back up will be slow and not in line with user expectations (eg scroll up in Facebook). I'll report back when I've implemented something... – poshest Apr 09 '14 at 21:29
1

You probably want to find the true bottleneck first before applying any optimizations. You can use Chrome DevTools with code snippets to profile your code, see http://bahmutov.calepin.co/improving-angular-web-app-performance-example.html

gleb bahmutov
  • 1,801
  • 15
  • 10
0

We also run into performance issues, UI started to stuck, it took a lot of time to data-bound object, after some tests we realized that ngRepeat adds $watch to each element and as a result has a direct impact on performance.

We decided that the best approach is to work with the DOM ourselves, we created our own ngRepeat directive and iterate the elements using the native JavaScript API (or JQuery, it doesn't matter), also we used CreateDocumentFragment for DOM manipulations (why would you want to use CreateDocumentFragment).

Example:

  mainApp.directive("myRepeater", function () {
   var LIST_ITEM = "li";
        return {            
            restrict: "A",
            link: function (scope, element, attrs) {
                var rawElm = element[0];        
                scope.$watch(attrs.source, function(newValue) {
                    if (!newValue || !newValue.length || newValue.length === 0) return;

                    // wipe the previous list
                    rawElm.innerHTML = "";
                    var frag = document.createDocumentFragment();

                    newValue.forEach(function (item) {
                        var listItemNd = document.createElement(LIST_ITEM);
                        var textNd = document.createTextNode("your text");
                        listItemNd.appendChild(textNd);
                        frag.appendChild(listItemNd);
                    });

                    rawElm.appendChild(frag);
                });
            }
        };
    });
Alex Choroshin
  • 6,177
  • 2
  • 28
  • 36
  • As far as I can tell, your solution is good for rendering static data, but doesn't support the repetition of rich Angular components such as the hypothetical `` object in my question. – poshest May 13 '16 at 12:05
-1

The answer in 2014 should have been: Yes, abandon Angular now.

Counter to what urban_racoons says, it's easy to have 100-200 items on a page that the user can see and meaningfully interact with.

Eg Google calendar on the 7 day display has 18 hours of 1/2 hour blocks showing on my monitor. If you had one ng-class for each one, you've got 252 watches right there. But you want ng-clicks, ng-ifs galore if you want all that Google calendar functionality.

On the other hand, in my own app, with only a header, some panels, a left nav menu (~5 items) and detail pane with ~5 tabs and ~30 items in the detail pane, I have >3000 watches (thanks to ng-stats).

Not only that, rendering just those 30 items takes over 2 seconds - Chrome devtools Profiles tab says it's all the controllersBoundTranscludes, nodeLinkFns and ngWatchIfActions (ng-if was supposed to be the saviour, but I have a few per item so time to evaluate those and render the result adds up).

The problem is I'm damned if I do (put ng-ifs everywhere means re-render time when showing the items hidden by the ng-if is slow, and even ng-infinite-scroll is sluggish) and damned if I don't (put ng-shows means the watches add up to unacceptable levels).

So I am having to resort to a bunch of hacks and optimisations, such as the

In my opinion these optimisations should come in the Angular core.

The harder question in 2016, with 1000s of lines of code already written: Should I abandon AngularJS now?

Community
  • 1
  • 1
poshest
  • 4,157
  • 2
  • 26
  • 37