Experiences with Angular performance profiling

Jing
6 min readSep 3, 2017

Recently during my NG development, I solved a performance issue with the help of Chrome profiling tool, and would like to share and record the process here.

This is my company internal product and still using Angular 1.x (no intention to explain why still 1.x, all in all, it’s enough). Generally it runs by fetching data from backend and then rendering some lists or charts for operation use. And my job is just to show the data using two different views. While since the two views are both lists(you may simply think that they are the same array ordered by different properties, but more than that), so I use the same template. Code like the following:

<!-- HTML template example, uses bootstrap accordion component --><div class="panel-group" id="accordion" role="tablist" aria-multiselectable="true">
<div class="panel panel-default" ng-repeat="item in fooLists">
<div class="panel-heading" role="tab">
<h3>
<a role="button" data-toggle="collapse" data-parent="#accordion" href="#fooPanel" aria-expanded="true">
</a>
</h3>
</div>
<div id="fooPanel" class="panel-collapse collapse" role="tabpanel" aria-expanded="true">
<div class="panel-body">...</div>
</div>
</div>
</div>
# Angular controller example for view switching buttons, uses coffeescript syntax$scope.switchView = (view) ->
switch view
when 'fooView1'
...
$scope.fooLists = getView1List()
when 'fooView2'
...
$scope.fooLists = getView2List()
return

Now I think it’s clear. Same $scope variable and same template, easy and simple. Now weird things happened when I clicked on the switch button to change to another view: it cost 2 or 3 seconds or so! But my list had only several hundred items and not thousands, it shouldn’t be that slow. Being really curious about the cause, I launched Chrome profiling tool and let’s try to find why.

1. Firstly, diagnose the cause first.

Look at the whole profiling picture before and after I clicked the view-switching button.

Fig 1. The profiling timeline chart

From the top timeline, we can see a long yellow area which indicates a lot of scripting(javascript) was involved. Was that just the cause? Take another look at the bottom ‘Summary’ tab.

Fig 2. Summary tab chart

Indeed it was, although incredible that JS executing can be that slow in our browser. Then I tried to find which piece of code led to this. From the ‘Bottom-up’ tab, I found the following.

Fig 3. Bottom-Up tab chart

setTimeout activity occupied most of the time. Then I expanded the call stack tree down to the deepest calling.

Fig 4. Call stack chart

OMG, unsurprisingly, it was, was… finally my own code, the so called xxxTooptip.js , which is a directive that’s based on the popular Bootstrap tooltip component. I doubted that it may be my improper use rather than bug of this public component. Inspect the code first.

Fig 5. My tooltip directive code

And my use in template:

<!-- Html the two view-switching buttons --><div class="btn-group" role="group" aria-label="..." xxx-tooltip>
<button class="btn btn-sm" ng-click="switchView('view1')" data-toggle="tooltip" data-original-title="xxx" >view1</button>
<button class="btn btn-sm" ng-click="switchView('view2')" data-toggle="tooltip" data-original-title="xxx">view2</button>
</div>

From the code we can see that it configures and binds click handler to every element which has data-toggle="tooltip" attribute. So every time an element with xxxTooltip directive is linked, the switching button will be bound with a new but duplicate handler. What if we use this directive on each item of a long list? Unfortunately it’s just my case. The click handlers of the switching button were invoked inefficiently hundreds of times! Well, it’s really bad case, though it’s typical code snippet copied from web resources. I believed that it’s at least one cause of the performance issue and generally we should limit the scope of a directive so it will not affect others. So try to fix it.

2. Code refactoring Round One.

I re-wrote the directive as following:

# use coffeescriptlink: (scope, element, attrs) ->  tooltipElem = element.find('[data-toggle="tooltip"]')  tooltipElem.tooltip(    container: 'body'    placement: 'top'    html: true    trigger: 'hover'  )  tooltipElem.on('click', ->    $(this).tooltip('hide')    return  )

This time only its own children of this directive will be bound with the click handler. So surely there will be only one handler invoking when we hit the button. Recompile and let’s see the changes.

Fig 6. Refactor round 1 chart

The most time-consuming scripting activity disappeared expectedly and it’s faster by 60% or so. Good improvement. But I still felt a pause when I switched between the buttons. Now the top activity is Major GC . I know GC is for Garbage Collection, but why was GC triggered here? Let’s dig further.

Fig 7. Major GC call stack chart

Again I expanded the call stack and found ngRepeat involved. Remember we used the same $scope variable and same template? While GC may be complex in details, this multiplexing pattern must be a major cause, since we always reassigned $scope.fooLists , the old array was GCed then DOM reordered… reasonable analysis. So how to fix it? How about using separate variables and templates? In this way, we should provide another NG directive ngShow to control visibilities of the two views and it will less likely to trigger GC frequently since all variables are kept in memory. Actually it is just my final solution. Go on.

3. Code refactoring Round Two.

Re-wrote the templates and controller.

<!-- templates -->
<div class="panel-group accordion" role="tablist" aria-multiselectable="true" ng-show="view === 'view1'">
<div class="panel panel-default" ng-repeat="item in fooLists1">
<div class="panel-heading" role="tab">
<h3>
<a role="button" data-toggle="collapse" data-parent="#accordion" href="#fooPanel" aria-expanded="true">
</a>
</h3>
</div>
<div id="fooPanel" class="panel-collapse collapse" role="tabpanel" aria-expanded="true">
<div class="panel-body">...</div>
</div>
</div>
</div>
<div class="panel-group accordion" role="tablist" aria-multiselectable="true" ng-show="view === 'view2'">
<div class="panel panel-default" ng-repeat="item in fooLists2">
<div class="panel-heading" role="tab">
<h3>
<a role="button" data-toggle="collapse" data-parent="#accordion" href="#fooPanel" aria-expanded="true">
</a>
</h3>
</div>
<div id="fooPanel" class="panel-collapse collapse" role="tabpanel" aria-expanded="true">
<div class="panel-body">...</div>
</div>
</div>
</div>
# Angular controller
$scope.view = 'view1'
$scope.switchView = (view) ->
$scope.view = view
return

So in this way, we should prepare two arrays $scope.fooLists1 and $scope.fooLists2each time we got data from backend, the switching will be merely changes of CSS styles. The final result:

Fig 8. Major GC after refactoring chart

GC decreased by 80% or so and now I felt smoothly when I switched the views. Although it has cons, e.g. extra cost for filling another list at the very beginning, I think I have achieved the desired optimization. All works well now.

Conclusion

From this successful profiling, I think there are at least three things worthy to be noted in NG development:

  • Try to limit the scope of your directives as much as possible. Directive in design should keep High Cohesion and Loose Coupling, just as the general rule for any other software design patterns.
  • Prepare different templates and use ngShow is often efficient way. You are essentially using more memory to achieve faster speed. It can be trade-off sometimes but worth trying.
  • Trust the profiling tool. Use this whenever you encounter performance issues.

Thanks for reading this quite detailed post and hope to share more in the future 😃

--

--

Jing

Breathtaking interfaces and strong services together make great products