Event Delegation Performance vs. Linking Performance In AngularJS
In the past, I've talked about deferring DOM (Document Object Model) tree transclusion, in AngularJS, as a means to boost performance. Deferring transclusion can help, on large collections, because it cuts down on the number of active $watch() bindings. But, how much of that performance gain can be attributed to the event delegation itself? Meaning, what is the cost of linking a directive on each sibling node as opposed to linking a single directive on a common ancestor?
Run this demo in my JavaScript Demos project on GitHub.
To experiment with this, I set up a demo that renders a list with 1,000 items. We're going to bind to the "mouseenter" event and then log out the text of the moused-into element. In one version, we're going to use a single directive at the list-root, which uses event delegation for the list items. Then, in the second version, instead of event delegation, we're going to link a directive to each individual list item.
First, using event delegation:
<!doctype html>
<html ng-app="Demo" ng-controller="AppController" bn-window-teaser>
<head>
<meta charset="utf-8" />
<title>
Event Delegation Performance vs. Linking Performance In AngularJS
</title>
<link rel="stylesheet" type="text/css" href="./demo.css"></link>
</head>
<body>
<h1>
Event Delegation Performance vs. Linking Performance In AngularJS
</h1>
<h2>
Event Delegation Performance
</h2>
<!--
In this approach, we're going to have a single "mouseenter" event handler
delegated to the UL directive.
-->
<ul bn-friends class="friends">
<li
ng-repeat="friend in friends track by friend.id"
class="friend">
{{ friend.name }}
</li>
</ul>
<!-- Load scripts. -->
<script type="text/javascript" src="../../vendor/jquery/jquery-2.1.0.min.js"></script>
<script type="text/javascript" src="../../vendor/angularjs/angular-1.3.6.min.js"></script>
<script type="text/javascript">
// Create an application module for our demo.
var app = angular.module( "Demo", [] );
// -------------------------------------------------- //
// -------------------------------------------------- //
// I control the root of the application.
app.controller(
"AppController",
function( $scope ) {
$scope.friends = [];
// Build up a large collection of friends so we can see how performance
// is affected at scale (basically the only time performance matters).
for ( var i = 1 ; i <= 1000 ; i++ ) {
$scope.friends.push({
id: i,
name: ( "Friend " + i )
});
}
}
);
// -------------------------------------------------- //
// -------------------------------------------------- //
// I provide a directive for the entire list, handling the "mouseenter" event
// at the common parent element.
app.directive(
"bnFriends",
function() {
// Return the directive configuration.
return({
link: link,
restrict: "A"
});
// I bind the JavaScript events to the scope.
function link( $scope, element, attributes ) {
// Listen for "mouseenter" events on the descendant LI elements.
element.on(
"mouseenter",
"li.friend",
function handleMouseEnter( event ) {
var target = angular.element( event.target );
console.log( "Mousing", angular.element.trim( target.text() ) );
}
);
}
}
);
</script>
</body>
</html>
As you can see, our directive is on the UL element, which is only linked a single time. It listens for the "mouseenter" event on the descendant LI elements.
NOTE: AngularJS doesn't allow for event delegation using .on(). This demo works because I am using jQuery as the angular.element() constructor function.
Now, let's look at the second version in which we use individual directives:
<!doctype html>
<html ng-app="Demo" ng-controller="AppController" bn-window-teaser>
<head>
<meta charset="utf-8" />
<title>
Event Delegation Performance vs. Linking Performance In AngularJS
</title>
<link rel="stylesheet" type="text/css" href="./demo.css"></link>
</head>
<body>
<h1>
Event Delegation Performance vs. Linking Performance In AngularJS
</h1>
<h2>
Linking Performance
</h2>
<!--
In this approach, we're going to link a directive for each friend in the
list, allowing each node to listen for its own mouse events.
-->
<ul class="friends">
<li
ng-repeat="friend in friends track by friend.id"
bn-friend
class="friend">
{{ friend.name }}
</li>
</ul>
<!-- Load scripts. -->
<script type="text/javascript" src="../../vendor/jquery/jquery-2.1.0.min.js"></script>
<script type="text/javascript" src="../../vendor/angularjs/angular-1.3.6.min.js"></script>
<script type="text/javascript">
// Create an application module for our demo.
var app = angular.module( "Demo", [] );
// -------------------------------------------------- //
// -------------------------------------------------- //
// I control the root of the application.
app.controller(
"AppController",
function( $scope ) {
$scope.friends = [];
// Build up a large collection of friends so we can see how performance
// is affected at scale (basically the only time performance matters).
for ( var i = 1 ; i <= 1000 ; i++ ) {
$scope.friends.push({
id: i,
name: ( "Friend " + i )
});
}
}
);
// -------------------------------------------------- //
// -------------------------------------------------- //
// I provide a directive for each element in the list, handling the "mousenter"
// event at the individual child level.
app.directive(
"bnFriend",
function() {
// Return the directive configuration.
return({
link: link,
restrict: "A"
});
// I bind the JavaScript events to the scope.
function link( $scope, element, attributes ) {
// Listen for the "mouseenter" event on the current node.
element.on(
"mouseenter",
function handleMouseEnter( event ) {
console.log( "Mousing", angular.element.trim( element.text() ) );
}
);
}
}
);
</script>
</body>
</html>
As you can see, in this version, we're going to be linking the bnFriend directive for each clone in the ngRepeat rendering.
Keep in mind that this isn't a perfect test and is probably affected by my computer and the fact that I'm using Chrome. But, when all is said and done, the event delegation approach was a bit faster. For 1,000 elements, the event delegation approach typically took between 225ms and 260ms to render. The individual linking approach typically took between 260ms and 315ms to render.
That said, both approaches fluctuated in performance. And, considering that event-delegation adds a level of complexity, I am not sure that the small difference in performance is worth it. Of course, this is a fairly isolated test; so, there may be other real-world factors that can influence the performance (such as the number of function declarations made inside the linking function).
Personally, I am delighted to see linking functions work so well on large sets. I would rather use that approach as I find it easier to reason about. Plus, it gives me more to consider when I think about using jQuery in an AngularJS application. One of the features that I love most about jQuery is event delegation. But, if event delegation isn't a huge win for performance, then it might not be a real "win" in an AngularJS application.
Want to use code from this post? Check out the license.
Reader Comments
I think that the Event Delegation example will have a slightly better performance in case some items will be added/removed from the array at runtime. That's because when using link() function in bnFriend directive (in the other example) , we have to remove the listener on $scope.$destroy with element.off() (or introduce a memory leak I guess). When using event delegation, there is no such a thing. I haven't test it, I might be wrong, it's just a thougt :)
Great article as always Ben! This surprised me a great deal. I surely thought using event delegation would give you a more significant performance boost. Like you said, it might be more favorable in a real world environment with larger link-functions. I'd be interested to know if anyone has experienced more significant performance gains in a production app using this method.
We need to support IE8 and quickly noticed when iterating over 100 ng-repeated items with ng-clicks or other events IE8 comes to a crawl.
We saw huge (seemed like magnitutes) IE8 performance improvements when we:
1) added a single jQuery DOM event handler using event delegation to target the elements in our list
2) assembled the html in javascript using Handlebars templates then made one DOM manipulation using $listRoot.html(html). We didn't even need to $compile the html because we added $scope.$apply to the jQuery event handlers.
Basically, without the above changes our app did not work in IE8. IE8 DOM manipulation is super slow, to say the least.
So Chrome may not show that much performance increase but browsers with slower javascript engines will.
Thanks for the post!
@Patrik,
Ah, really good point. I was just looking at the initial load / processing time. But, you're totally right - each addition to the collection would require the directive to be linked. It's a tiny bit of processing, but I hadn't considered it in this exploration.
@Niklas,
Also, keep in mind that I have a fairly new computer with lots of RAM and a SSD harddrive... so, I've got a lot of power backing the exploration. As such, the mileage may vary on different systems.
Of course, there are other reasons, beyond performance, to use event delegation. If you [the directive] don't have control / knowledge of what is going to load, then event delegation still let's you bind events without the existing of DOM elements, which is what makes it so powerful.
@Philip,
Awesome feedback! And real-world experience is worth more than isolated theory, as far as I'm concerned. Performance is one of those funny things in which the tooling required to measure it somewhat limits how I test it in the first place. I know a lot(ish) about the Chrome tools, so that's where I do most of this kind of testing.
Can I ask how you were testing IE8? Meaning, was it on an actual Windows machine? Or were you using a Virtual Machine with IE installed? I ask because my Virtual Machine generally sucks, across the board.
@Ben
We remote into a VMWare Windows XP virtual machine running on a VMWare server. We have noticed some glitches that we were surprised clients haven't complained about so we concluded it's because of the VM.
In April we're dropping support for IE8 so we can't wait to switch to the 1.3 branch to take advantage of all the performance improvements. We use angular-once library right now but since that's build into angular in the 1.3 branch we'll use the native one-time bindings.
Philip