In het kader 'handig' dacht ik eens een topicje te openen speciaal voor de search, maar ook met wat diepere achtergrondinformatie ter leering ende vermaeck. Dit is dus niet puur bedoelt als een soort codebase-achtig topic maar omvat meer dan dat.
Ik moest voor een script bepaalde elementen uit de DOM tree op basis van classes kunnen selecteren, en dus was het idee om een getElementsByClassName method te schrijven geboren. In het verleden heb ik al diverse malen dergelijke scripts geschreven, en uit ervaring wist ik ongeveer ook wel wat efficient zou zijn. In dit geval wou ik echter mijn eigen implementatie eens toetsen en was dus vervolgens ook maar eens op zoek gegaan naar soortgelijke implementaties op het internet. Dat bracht me al gauw op deze pagina waar wat veelbelovende, en ook goed doordachte, scriptlets stonden. Ik ben daarmee dus ook aan het stoeien geweest en heb ook in diverse browsers eens wat benchmarks gedraait. De bevindingen zijn opmerkelijk te noemen.
Laten we eens met de meest simpele manier beginnen, gebaseerd op getElementsByTagName('*'), wat een erg 'dure' methode lijkt te zijn, maar wel het meest compacte script oplevert:
Dit is een ietwat meer geoptimaliseerde versie van de functie op bovengenoemde pagina. De methode om ook een bepaalde class in een class-attribuut met meerdere classes te kunnen vinden vond ik op zich wel vindingrijk hoewel mijn voorkeur uitgaat naar een reguliere expressie (die zal ik straks laten zien).
Ik heb dit script (en alle volgende scripts) losgelaten op een gesaved topic van GoT met 50 messages en heb 'm laten zoeken naar elementen met als class 'message', op deze manier:
De tc variabele heb ik in de getElementsByClassName ook het totaal aantal elementen laten tellen om er zeker van te zijn dat alle implementaties ook daadwerkelijk alle elementen in de pagina af zijn gegaan. Niets meer dan een controle getal dus, dus doet verder ook weinig ter zake. Er zaten in het opgeslagen document trouwens in totaal 5310 elementen, waarvan 50 elementen met de 'message'-class. Overigens hadden deze elementen naast de message class ook nog een class 'altmsg1' of 'altmsg2', zeg maar in deze vorm:
Je kan dus niet puur op e.className == needle checken; dat zou te beperkt zijn en niet het gewenste resultaat opleveren.
Hoe snel was dat script nou eigenlijk? Ik merkte net al op dat getElementsByTagName('*') erg duur lijkt te zijn, maar dat lijkt helemaal aan de gebruikte browser te liggen. Ik heb me beperkt tot recente versies van de meestgebruikte browsers; het testteam bestond uit:
1) Firefox 1.01 (nog geen tijd gehad 1.02 te installeren
)
2) IE6.0 met alle beschikbare patches voor mijn platform (te weten windows 2000)
3) Opera 7.54
Dat alles op een AMD XP2000+ (niet echt meer een krachtmonster dus hedentendage)
Genoeg gelult, hier zijn de resultaten voor de eerste poging tot een snelle getElementsByClassName method:
Conclusie: getElementsByTagName('*') is inderdaad een hele dure method, behalve voor Firefox. De laatste zal daar waarschijnlijk intern een bepaalde optimalisatie voor hebben ingebouwd, wat in dit geval goed van pas komt, maar aan de andere kant nutteloos is omdat de snelheid in andere browsers dusdanig bedroefend is dat het in de praktijk geen nut heeft deze constructie te gebruiken.
De volgende methode (ook van eerdergenoemde pagina) leek me dan ook een stuk veelbelovender:
(wederom ietwat aangepast naar mijn eigen smaak en style en met wat kleine performance verbeteringen)
Deze methode gebruikt recursie om door de DOM heen te lopen en daar alle elementen uit te vissen. De vraag is echter of dat inderdaad sneller is dan het gebruik van getElementsByTagName('*'). Mijn eerste gedacht zou zijn: nee, aangezien je in feite precies hetzelfde doet, maar dan op een eigen manier. Ik zou dus zelfs eerder verwachten dat het langzamer is, maar laten we de benchmarks maar voor zich spreken:
Kortom: geen enkel verschil! Kan het dan gewoon niet sneller? En nog belangrijker: kan het voornamelijk in non-Firefox browsers dan niet sneller?
De pagina waar ik deze scripts vandaan heb noemde nog 2 alternatieven; 1 gebaseerd op XPath die in dit geval onbruikbaar was omdat hij geen elementen kan selecteren waarvan het class-attribuut meerdere classes bevat (puur gebaseerd op equality), en 1 met behulp van een TreeWalker die volgens de genoemde site Gecko-only is (dus sowieso al niet bruikbaar om die reden), maar die in Firefox gewoonweg niet eens werkte.
Toen was het tijd om mijn eigen probeersel maar eens te testen. De meeste DOM-walers die ik in het verleden heb gebruikt waren stack-based, en dankzij de recursie-based versie hierboven heb ik die zelfs nog wat weten te verbeteren.
Het script:
In feite doet dit script niets anders als de recursieve variant, behalve dan dat het elementen die nader onderzocht moeten worden op een stack zet, en vervolgens de stack wordt afgelezen totdat 'ie leeg is. Je zou denken dat dit qua instructies meer overhead oplevert (tegenover minder memory gebruik), maar dit blijkt toch een winner qua performance:
... in de non-Firefox-browsers that is
Op z'n minst opmerkelijk dus, en zelfs 550 milliseconden in Firefox is nog wel overheen te komen op zo'n relatief grote lap HTML, hoewel het heel vreemd is dat het juist in Firefox trager is, maar in de andere browsers zo ontzettend veel sneller...
Tot nog toe heb ik de truuk met spaties gebruikt om in een lijst van (eventueel) spatie-gescheiden classes naar een bepaalde class te zoeken. Ik noemde in het begin al dat een reguliere expressie hierin toch mijn voorkeur zou hebben. Dit omdat het a) minder code oplevert, en b) misschien zelfs wel sneller is aangezien een reguliere expressie, indien vooraf geinstantieerd, een soortement van compiled object is en daarom misschien zelfs wel sneller zou kunnen zijn. Dit is de reguliere expressie die ik hiervoor gemaakt heb:
En de laatst genoemde methode zou er met het gebruik van deze expressie zo uitzien:
En natuurlijk de benchmarks:
In Firefox dus (helaas) geen verschil, in IE weer een kleine performancewinst, en in Opera zelfs nog meer performancewinst.
Ok, we hebben dus een getElementsByClassName method die in elke browser voor grote documenten bruikbaar is qua performance. Jammer genoeg blijft Firefox qua performance achterlopen bij het gebruik van een stack-based treewalker, maar ook daar is het resultaat nog wel acceptabel.
Verder illustreren deze scripts wel de grote kracht van de DOM, en het feit dat dit soort scripts in bijna elke moderne browser werken zonder enige vorm van browsersniffing is een goed teken*
*Als laatste moet ik hierbij een voetnoot plaatsen met betrekking tot IE5.0 die niet op het gebied van DOM, maar wel op het gebied van javascript tekort schiet. Deze browser kent namelijk niet de push() en pop() methodes voor het Array-object. Javascript laat ons deze echter makkelijk toevoegen door middel van prototyping:
Uiteraard kan je binnen je code ook het gebruik van push en pop omzeilen door zelf array-pointers bij te houden; op zich kost dat geen tot weinig extra performance, en je script werkt ook zonder prototyping in IE5.0:
Ik moest voor een script bepaalde elementen uit de DOM tree op basis van classes kunnen selecteren, en dus was het idee om een getElementsByClassName method te schrijven geboren. In het verleden heb ik al diverse malen dergelijke scripts geschreven, en uit ervaring wist ik ongeveer ook wel wat efficient zou zijn. In dit geval wou ik echter mijn eigen implementatie eens toetsen en was dus vervolgens ook maar eens op zoek gegaan naar soortgelijke implementaties op het internet. Dat bracht me al gauw op deze pagina waar wat veelbelovende, en ook goed doordachte, scriptlets stonden. Ik ben daarmee dus ook aan het stoeien geweest en heb ook in diverse browsers eens wat benchmarks gedraait. De bevindingen zijn opmerkelijk te noemen.
Laten we eens met de meest simpele manier beginnen, gebaseerd op getElementsByTagName('*'), wat een erg 'dure' methode lijkt te zijn, maar wel het meest compacte script oplevert:
JavaScript:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
| document.getElementsByClassName = function (needle) { var s = document.getElementsByTagName('*'), i = s.length, r = [], e, c; needle = ' ' + needle + ' '; while (i--) { e = s.item(i); if (e.className) { c = ' ' + e.className + ' '; if (c.indexOf(needle) != -1) r.push(e); } } return r; } |
Dit is een ietwat meer geoptimaliseerde versie van de functie op bovengenoemde pagina. De methode om ook een bepaalde class in een class-attribuut met meerdere classes te kunnen vinden vond ik op zich wel vindingrijk hoewel mijn voorkeur uitgaat naar een reguliere expressie (die zal ik straks laten zien).
Ik heb dit script (en alle volgende scripts) losgelaten op een gesaved topic van GoT met 50 messages en heb 'm laten zoeken naar elementen met als class 'message', op deze manier:
JavaScript:
1
2
3
4
5
6
7
8
9
10
11
| var tc; function bench() { var st = new Date().getTime(); tc = 0; var r = document.getElementsByClassName('message'); var et = new Date().getTime(); alert (r.length + ' items found in ' + (et-st) + ' milliseconds; ' + tc + ' elements total'); } |
De tc variabele heb ik in de getElementsByClassName ook het totaal aantal elementen laten tellen om er zeker van te zijn dat alle implementaties ook daadwerkelijk alle elementen in de pagina af zijn gegaan. Niets meer dan een controle getal dus, dus doet verder ook weinig ter zake. Er zaten in het opgeslagen document trouwens in totaal 5310 elementen, waarvan 50 elementen met de 'message'-class. Overigens hadden deze elementen naast de message class ook nog een class 'altmsg1' of 'altmsg2', zeg maar in deze vorm:
HTML:
1
| <div class="message altmsg1"> |
Je kan dus niet puur op e.className == needle checken; dat zou te beperkt zijn en niet het gewenste resultaat opleveren.
Hoe snel was dat script nou eigenlijk? Ik merkte net al op dat getElementsByTagName('*') erg duur lijkt te zijn, maar dat lijkt helemaal aan de gebruikte browser te liggen. Ik heb me beperkt tot recente versies van de meestgebruikte browsers; het testteam bestond uit:
1) Firefox 1.01 (nog geen tijd gehad 1.02 te installeren

2) IE6.0 met alle beschikbare patches voor mijn platform (te weten windows 2000)
3) Opera 7.54
Dat alles op een AMD XP2000+ (niet echt meer een krachtmonster dus hedentendage)
Genoeg gelult, hier zijn de resultaten voor de eerste poging tot een snelle getElementsByClassName method:
Firefox: 340 milliseconden IE: 15400 milliseconden Opera: 19300 milliseconden
Conclusie: getElementsByTagName('*') is inderdaad een hele dure method, behalve voor Firefox. De laatste zal daar waarschijnlijk intern een bepaalde optimalisatie voor hebben ingebouwd, wat in dit geval goed van pas komt, maar aan de andere kant nutteloos is omdat de snelheid in andere browsers dusdanig bedroefend is dat het in de praktijk geen nut heeft deze constructie te gebruiken.
De volgende methode (ook van eerdergenoemde pagina) leek me dan ook een stuk veelbelovender:
JavaScript:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
| document.getElementsByClassName = function (needle) { function _GetElementsByClass(r, e, needle) { while (e) { if (e.nodeType == 1) { if (e.className) { c = ' ' + e.className + ' '; if (c.indexOf(needle) != -1) r.push(e); } _GetElementsByClass(r, seed.e, needle) } e = e.nextSibling; } } needle = ' ' + needle + ' '; var r = [], c; _GetElementsByClass(r, document.documentElement, needle); return r; } |
(wederom ietwat aangepast naar mijn eigen smaak en style en met wat kleine performance verbeteringen)
Deze methode gebruikt recursie om door de DOM heen te lopen en daar alle elementen uit te vissen. De vraag is echter of dat inderdaad sneller is dan het gebruik van getElementsByTagName('*'). Mijn eerste gedacht zou zijn: nee, aangezien je in feite precies hetzelfde doet, maar dan op een eigen manier. Ik zou dus zelfs eerder verwachten dat het langzamer is, maar laten we de benchmarks maar voor zich spreken:
Firefox: 340 milliseconden IE: 15400 milliseconden Opera: 19300 milliseconden
Kortom: geen enkel verschil! Kan het dan gewoon niet sneller? En nog belangrijker: kan het voornamelijk in non-Firefox browsers dan niet sneller?
De pagina waar ik deze scripts vandaan heb noemde nog 2 alternatieven; 1 gebaseerd op XPath die in dit geval onbruikbaar was omdat hij geen elementen kan selecteren waarvan het class-attribuut meerdere classes bevat (puur gebaseerd op equality), en 1 met behulp van een TreeWalker die volgens de genoemde site Gecko-only is (dus sowieso al niet bruikbaar om die reden), maar die in Firefox gewoonweg niet eens werkte.
Toen was het tijd om mijn eigen probeersel maar eens te testen. De meeste DOM-walers die ik in het verleden heb gebruikt waren stack-based, en dankzij de recursie-based versie hierboven heb ik die zelfs nog wat weten te verbeteren.
Het script:
JavaScript:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
| document.getElementsByClassName = function (needle) { var s = [], r = [], c, undefined; var e = document.documentElement || document.body; needle = ' ' + needle + ' '; while (e !== undefined) { while (e) { if (e.nodeType == 1) { if (e.className) { c = ' ' + e.className + ' '; if (c.indexOf(needle) != -1) r.push(e); } s.push(e.firstChild); } e = e.nextSibling; } e = s.pop(); } return r; } |
In feite doet dit script niets anders als de recursieve variant, behalve dan dat het elementen die nader onderzocht moeten worden op een stack zet, en vervolgens de stack wordt afgelezen totdat 'ie leeg is. Je zou denken dat dit qua instructies meer overhead oplevert (tegenover minder memory gebruik), maar dit blijkt toch een winner qua performance:
Firefox: 550 milliseconden IE: 280 milliseconden Opera: 120 milliseconden
... in de non-Firefox-browsers that is

Tot nog toe heb ik de truuk met spaties gebruikt om in een lijst van (eventueel) spatie-gescheiden classes naar een bepaalde class te zoeken. Ik noemde in het begin al dat een reguliere expressie hierin toch mijn voorkeur zou hebben. Dit omdat het a) minder code oplevert, en b) misschien zelfs wel sneller is aangezien een reguliere expressie, indien vooraf geinstantieerd, een soortement van compiled object is en daarom misschien zelfs wel sneller zou kunnen zijn. Dit is de reguliere expressie die ik hiervoor gemaakt heb:
JavaScript:
1
| var re = new RegExp('(^|\\s)' + needle + '(\\s|$)'); |
En de laatst genoemde methode zou er met het gebruik van deze expressie zo uitzien:
JavaScript:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
| document.getElementsByClassName = function (needle) { var s = [], r = [], undefined; var e = document.documentElement || document.body; var re = new RegExp('(^|\\s)' + needle + '(\\s|$)'); while (e !== undefined) { while (e) { if (e.nodeType == 1) { if (e.className && re.test(e.className)) r.push(e); s.push(e.firstChild); } e = e.nextSibling; } e = s.pop(); } return r; } |
En natuurlijk de benchmarks:
Firefox: 550 milliseconden IE: 270 milliseconden Opera: <100 milliseconden
In Firefox dus (helaas) geen verschil, in IE weer een kleine performancewinst, en in Opera zelfs nog meer performancewinst.
Ok, we hebben dus een getElementsByClassName method die in elke browser voor grote documenten bruikbaar is qua performance. Jammer genoeg blijft Firefox qua performance achterlopen bij het gebruik van een stack-based treewalker, maar ook daar is het resultaat nog wel acceptabel.
Verder illustreren deze scripts wel de grote kracht van de DOM, en het feit dat dit soort scripts in bijna elke moderne browser werken zonder enige vorm van browsersniffing is een goed teken*
*Als laatste moet ik hierbij een voetnoot plaatsen met betrekking tot IE5.0 die niet op het gebied van DOM, maar wel op het gebied van javascript tekort schiet. Deze browser kent namelijk niet de push() en pop() methodes voor het Array-object. Javascript laat ons deze echter makkelijk toevoegen door middel van prototyping:
JavaScript:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
| if (typeof Array.prototype.push == 'undefined') { Array.prototype.push = function() { var l = this.length, i = 0, j = arguments.length; while (i < j) this[l++] = arguments[i++]; return l; } } if (typeof Array.prototype.pop == 'undefined') { Array.prototype.pop = function() { var l = this.length, r; if (l) { r = this[--l]; this.length = l; } return r; } } |
Uiteraard kan je binnen je code ook het gebruik van push en pop omzeilen door zelf array-pointers bij te houden; op zich kost dat geen tot weinig extra performance, en je script werkt ook zonder prototyping in IE5.0:
JavaScript:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
| document.getElementsByClassName = function (needle) { var s = [document.documentElement || document.body], i = 0, r = [], l = 0, e; var re = new RegExp('(^|\\s)' + needle + '(\\s|$)'); do { e = s[i]; while (e) { if (e.nodeType == 1) { if (e.className && re.test(e.className)) r[l++] = e; s[i++] = e.firstChild; } e = e.nextSibling; } } while (i--); return r; } |
[Voor 3% gewijzigd door crisp op 04-04-2005 10:58]
Intentionally left blank