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.
Scenario: you have elements in your document and you need to change their color, background, or something else related to theirs style. For example, highlight table rows on mouse over or mark them when corresponding checkbox is checked.
And again I know two ways: using styles or set foreground(or background) color directly from JavaScript. Let’s test them before discussion:
1 2 3 4 | var items = el.getElementsByTagName('li'); for (var i = 0; i < 1000; i++) { items[i].className = 'selected'; } |
The average result is 187 – 291 ms, for the InternetExplorer 6 it is 512 ms, and in Opera 9 it is 47 ms.
1 2 3 4 5 | var items = el.getElementsByTagName('li'); for (var i = 0; i < 1000; i++) { items[i].style.backgroundColor = '#007f00'; items[i].style.color = '#ff0000'; } |
I got results starting from 282 ms in Opera and ending with 1709 ms in Internet Explorer 6.
Result are clean and easy to understand:
No | Method | IE 6 | IE 7 | FF 1.5 | FF 2.0 | Opera 9 |
---|---|---|---|---|---|---|
1 | element.className | 512 | 187 | 291 | 203 | 47 |
2 | element.style.color | 1709 | 422 | 725 | 547 | 282 |
You can view benchmark test and get your own results here.
Looks like this is simplest optimization tip, but... there is one issue with Internet Explorer -- page update. Are you remember scenario described in the beginning of chapter, about onmouseover? When you change element's class name, your code is much faster, but the page would not be updated immediately. Look at this example. Try to click "Generate elements" when "element.className" radio button is selected. Then move your mouse pointer over the items, scroll list to the bottom, move mouse again (for slow machines items number would be less than default, for fast ones - greater). Have you noticed, that background changes not so fast as the mouse pointer moves? Now switch selected radio button to "element.style.color". Background changing smoothly, right?
At the bottom of page you see the number of onmouseover events triggered and average time spent to dispatch them. As you noticed, first radio button works two times faster! Why it looks slower? I think it is because when you changed className property Internet Explorer isn't actually updates UI, but placed event in update queue. If you have other ideas, please post comments.
Anybody, who read article up to this place, would probably say: "Hey, guy, you are cheating. Where is the :hover?". I'm hurry up to fix this shortcoming and describing this case right now. First, this thing is not working in the Internet Explorer 6 (and don't ask me "who need it?"). But this is not most difficult problem, performance of this issue is much worse. Even don't want to comment it, just select third radiobutton on previously given page and move your mouse in Internet Explorer 7 and Opera 9 (Firefox works great).
This is first article in 7 parts tutorial, stay tuned.
Scenario: you’re developing rich Internet application and you need to load dynamic elements using AJAX and then add them to current document. For some reason you don’t want (or can’t) use fully generated HTML, and decided to fetch items in JavaScript array.
I know two classic ways to do so: create elements using document.createElement() method and concatenate HTML string and assign it to parentElement.innerHTML property. Of course, you can combine both ways. Let’s examine this ways in details.
Classic way (and in ideal world the best way) is to use DOM for element manipulations:
1 2 3 4 5 | for (var i = 1; i <= 1000; i++) { var li = document.createElement('li') li.appendChild(document.createTextNode('Element ' + i)); el.appendChild(li); } |
Not so bad performance. Internet Explorer 6 is slowest – 1403 ms (but it is a slower browser in the world, right?), and other browser displayed results quickly (63 – 328 ms). Ok, but what about creating DOM element from HTML code?
1 2 3 4 | for (var i = 1; i <= 1000; i++) { var li = document.createElement('<li>Element ' + i + '</li>'); el.appendChild(li); } |
It works better in Internet Explorer 6 (1134 ms), but does not work in other browsers at all. Weird! Of course, you can add try/catch block and create elements using first approach in catch block for the other browsers. But I have better solution.
Every DOM node has attribute innerHTML which holds all child nodes as HTML string.
1 2 3 4 | el.innerHTML = ''; for (var i = 1; i <= 1000; i++) { el.innerHTML += '<li>Element ' + i + '</li>'; } |
Wow, I’m highly impressed how slow could be adding elements procedure (11391 – 307938 ms)! Cool result, right? It’s because browser tried to render list while we updating and it’s take so long time. Little optimization:
1 2 3 4 5 | var html = ''; for (var i = 1; i <= 1000; i++) { html += '<li>Element ' + i + '</li>'; } el.innerHTML = html; |
All browsers shows great perfomance (31 – 109 ms), but Internet Explorer is still slow – 10994 ms. I found solution which works very fast in all browsers: to create array of HTML chunks, and the join them using empty string:
1 2 3 4 5 6 7 | var html = []; for (var i = 1; i <= 1000; i++) { html.push('<li>Element '); html.push(i); html.push('</li>'); } el.innerHTML = html.join(''); |
It’s fastest approach for Internet Explorer 6 – 400 ms, and very fast in other browsers. Why I’m not saying fastest in case of Firefox? I added another test to make in cleaner:
1 2 3 4 5 6 7 | var html = ''; for (var i = 1; i <= 1000; i++) { html += '<li style="padding-left: ' + (i % 50) + '" id="item-' + i + '">Element ' + i + ' Column ' + (i % 50) + '</li>'; } el.innerHTML = html; |
And second example:
1 2 3 4 5 6 7 8 9 10 11 12 13 | var html = []; for (var i = 1; i <= 1000; i++) { html.push('<li style="padding-left: '); html.push(i % 50); html.push('" id="item-'); html.push(i); html.push('">Element '); html.push(i); html.push(' Column '); html.push(i % 50); html.push('</li>'); } el.innerHTML = html.join(''); |
Here are the results in table and diagram formats.
No | Method | IE 6 | IE 7 | FF 1.5 | FF 2.0 | Opera 9 |
---|---|---|---|---|---|---|
1 | createElement() | 1403 | 219 | 166 | 328 | 63 |
2 | createElement() full | 1134 | - | - | - | - |
3 | innerHTML | 39757 | 20781 | 41058 | 307938 | 11391 |
4 | innerHTML optimized | 10994 | 46 | 50 | 109 | 31 |
5 | innerHTML/join | 400 | 31 | 47 | 125 | 31 |
6 | innerHTML/optimized+ | 28934 | 109 | 84 | 172 | 62 |
7 | innerHTML/join+ | 950 | 78 | 110 | 189 | 62 |
You can view benchmark test and get your own results here.