This is a third part of the JavaScript optimization tutorial, and today I’m going to talk about events. Sorry for a long delay between posts, I hope remaining parts would not be delayed so much.
Scenario: you have some elements and you need to add some actions to them (for example, when user moves mouse cursor over element, or clicks on elements).
This is pretty usual task in web-development. And the first thing that everybody knows is a different event attaching syntax in Internet Explorer and Firefox: first uses element.attachEvent, second — element.addEventListeners. Therefor you need to add code, which would detect what browser is being used and what method to use in each case. In the most popular client script library Prototype it is already standardized and you can always use Event.observe. Let’s benchmark a little.
I’m going to start attaching events from manual detection of which browser is being used:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 | // Attaching events for (var i = items.length; i--; ) { if (items[i].addEventListener) { items[i].addEventListener('click', e_onclick, false); } else if (items[i].attachEvent) { items[i].attachEvent('onclick', e_onclick); } } // Detaching events for (var i = items.length; i--; ) { if (items[i].removeEventListener) { items[i].removeEventListener('click', e_onclick, false); } else if (items[i].detachEvent) { items[i].detachEvent('onclick', e_onclick); } } |
This approach shown best times: 188 and 203 ms in Internet Explorer 6 and 7, 125 and 141 ms in Firefox 1.5 and 2.0, and 63 ms in Opera 9, but there is one problem with it — memory leaks: Internet Explorer usually forget to clean up memory, used by event listeners, when you navigating to another page. Therefor all JavaScript frameworks which implement event attaching/detaching functions, provide functionality for removing listeners when you navigating to another page. Let’s test them.
Code for testing Prototype library:
1 2 3 4 5 6 7 8 | // Attaching events for (var i = items.length; i--; ) { Event.observe(items[i], 'click', e_onclick, false); } // Detaching events for (var i = items.length; i--; ) { Event.stopObserving(items[i], 'click', e_onclick, false); } |
This is very slow, 6453 ms in Internet Explorer 6 and looks like some memory leaks are still exists (it became slower and slower with the lapse of time) and 365 — 653 ms in other browsers.
There are tons of problems and solutions with adding events (just look at the Advanced event registration models). Some time ago even PPK’s addEvent() Recoding Contest was over (but do not use the winner’s solution because of it’s inefficiency and memory leaks).
Instead of winner’s solution, I decided to test incredible and impertinent solution from Dean Edwards, which even does not use the addeventListener/attachEvent methods, but it is entirely cross-browser!
Used code:
1 2 3 4 5 6 7 8 | // Attaching events for (var i = items.length; i--; ) { addEvent(items[i], 'click', e_onclick); } // Detaching events for (var i = items.length; i--; ) { removeEvent(items[i], 'click', e_onclick); } |
Results are as much impressive as when we used manual events attaching, and this approach is extremely fast to use it in real-world applications. Of course, you would say: “Stop, what you are talking about? I don’t want to use additional methods while Prototype is already used in my code!” Calm down, you don’t need to do it, just download Low Pro library (current version is 0.4, stay tuned). Ruby on Rails developers could use UJS Rails Plugin in their applications to improve performance — it uses Low Pro inside.
No | Method | IE 6 | IE 7 | FF 1.5 | FF 2.0 | Opera 9 |
---|---|---|---|---|---|---|
1 | manually | 203 | 188 | 125 | 141 | 63 |
2 | prototype.js | 6453 | 653 | 547 | 469 | 365 |
3 | addEvent | 783 | 344 | 94 | 141 | 62 |
You can review benchmark and get your own results here.
Conclusions
- Always use Low Pro along with Prototype.js (of course, if your application uses rich client logic).
- Use manual events attaching when you need to get as much as possible speed from your application (for example, when you handling hundreds of elements). But don't forget about memory leaks!
- Avoid events attaching whenever it's possible (for example, use :hover CSS selector instead of onmouseover event).
- Keep your eyes opened, maybe tomorrow somebody would post another great performance optimization tip.
Links to other parts
- Part 1: Adding DOM elements to document
- Part 2: Applying styles to elements
- Part 3: Attaching events
- Part 4: Multiple anonymous functions (will be published soon)
- Part 5: Attaching events on demand (will be published soon)
- Part 6: Element hide and show (will be published soon)
- Part 7: Elements collection enumeration (will be published soon)
Thanks for really interesting post and for mentioning Low Pro.
Optimalizace JavaScriptu…
Dmitrij Štefljuk píše velmi zajímavý seriál o optimalizaci JavaScriptu. Doporučuji všem, co se zabývají JavaScriptem.
…
Great article,
One thing to take into consideration about applying events (apart from “how”) is “where”.
I mean, for example, it’s far better to attach ONE “mouseover” event to a whole “tbody” and capture the “td” source which fired it than to attach many of “mouseover”s to ALL “td”s…
So, for speed reasons it is crucial “where” do you attach your events…
Thank you, Enrique
You right, of course. It’s better to use one event observer for parent element instead of separate observers for each child node. Great tip!
[…] JavaScript optimization Part 3: Attaching events […]
[…] Что лучше использовать при подписке на события – фреймвор или делать это вручную? ответ здесь […]
очень много дельных советов можно почерпнуть из этой статьи
ps. OpenID не отработал
Спасибо за ссылку и за баг :-) Будем посмотреть
I’m sure also, that Your code using “manual” events setting would be little faster if you checking event’s attaching methods existence once. On attaching example:
2
3
4
5
6
7
8
9
10
if (items[0].addEventListener) {
for (var i = items.length; i--; ) {
items[i].addEventListener('click', e_onclick, false);
}
} else if (items[0].attachEvent) {
for (var i = items.length; i--; ) {
items[i].attachEvent('onclick', e_onclick);
}
}
Look also on brilliant idea in one of last slide of Dan Webb’s presentation here:
http://www.danwebb.net/2007/11/22/media-ajax
btw. thx for the link to Low Pro :-)
UJS Rails Plugin rules!