getElementsByClassName
Vanwege separation of concerns en de mogelijkheid voor graceful degradation1 is het handig om javascriptcode te scheiden van HTML-opmaakcode. Dit houdt in dat het toevoegen van interactie aan het HTML-document wordt gedaan in een apart javascript-bestand. Een handige manier om dit te doen is om gebruik te maken van classes, maar bij grote documenten kan dit leiden tot traag startende webapplicaties en dat heeft vervelende consequenties, zoals te zien in het dialoog van figuur 1. In dit artikel wordt een mogelijke oplossing geboden.

Figuur 1: Dialoog van Internet Explorer: Script stoppen?
Het scheiden van structuur, opmaak en interactie
Het belang van het scheiden van structuur, opmaak en interactie zal de meesten wel bekend zijn. Door deze drie concerns uit elkaar te houden worden applicaties eenvoudiger om te onderhouden. Opmaak wordt beschreven in css-bestanden en interactie in javascript-bestanden. Voorbeeld 1 is een voorbeeld waarbij de concerns niet zijn gescheiden, in voorbeeld 2 is aangegeven hoe deze gescheiden kunnen worden.
HTML
<input type="text" style="background-color: yellow;" onblur="validateField(this)"/>
Voorbeeld 1: Structuur, interactie en opmaak zijn niet gescheiden
HTML
<input id="telephone" type="text" onblur="validateTelephoneNumber(this)"/>
CSS
.telephone-number { background-color: yellow; }
Javascript
document.getElementById("telephone").onblur = function() { validateTelephoneNumber(this); }
Voorbeeld 2: Structuur, interactie en opmaak zijn wel gescheiden
Werken met classes
Het kan natuurlijk voorkomen dat dezelfde interactie voor meerdere elementen in een formulier gelden. Door naar invulvelden op class te verwijzen, in plaats van op id, kan met weinig code interactie consistent worden toegepast. Zo kunnen in een keer alle links met een ‘open-in-new’-annotatie worden aangepast zodat ze in een nieuw venster openen (zie voorbeeld 3).
HTML
<li><a class="open-in-new" href="http://www.w3.org>W3C<a></li>
<li><a class="open-in-new" href="www.oasis-open.org">OASIS</a></li>
<li><a href="#sources">andere bronnen</a></li>
Javascript
var openInNew = document.getElementsByTagName("a").getElementsByClassName("open-in-new");
for( var i = 0; i < openInNew.length; ++i ) {
openInNew[i].onclick = function() { openInNewWindow(this); }
}
Voorbeeld 3: Implementeren van interactie op basis van classes2
Performance
Het implementeren van interactie op basis van classes kan bij grote pagina's leiden tot een prestatieprobleem als het zoeken van elementen op basis van class niet efficiënt gebeurt. In Firefox 3 zit momenteel een native implementatie van getElementsByClassName, maar in Internet Explorer 6 en Internet Explorer 7 is dit niet het geval, waarbij het initialiseren van de pagina soms meer dan 10 seconde duurt. Er zijn verschillende javascript implementaties, maar de prestaties in Internet Explorer 6 en 7 blijven ook dan laag.
Om dit probleem te ondervangen kan vooraf een opzoektabel (zie voorbeeld 4) worden gemaakt waarin staat welke elementen een annotatie hebben op basis van het element id. Vervolgens kunnen de elementen worden opgezocht in de tabel in plaats van het document te moeten doorzoeken. Deze aanpak heeft als voordeel een snelheidswinst (zie beneden voor meer hierover), maar er zijn ook nadelen:
- alle elementen met een annotatie in het class attribuut moeten een id hebben
- als een annotatie wordt toegevoegd of verwijderd moet de opzoektabel worden bijgewerkt
- de opzoektabel is statisch: dynamisch toegevoegde of verwijderde elementen worden niet opgemerkt
- het laden van de tabel kost tijd
Deze nadelen maken dat het eerder een laatste redmiddel moet zijn, voor het geval andere methoden niet werken. De eerste twee punten zijn op te lossen door een script te draaien om de tabel automatisch te genereren. Een voorbeeld voor een dergelijk script is te vinden in de testbestanden. Voor de laatste twee punten moet per geval een afweging worden gemaakt.
Voorbeeld 4: een regel in de opzoektabel voor elementen met de class 'warning'
CachedElementsByClassName.cache['warning']=['id259225','id258252','id258181','id207591'];
Snelheid van de opzoektabel
Om een idee te geven van het verschil die deze oplossing biedt heb ik een test opgezet die vier methoden test3:
- Een naïve implementatie: doorlopen van boomstructuur door middel van DOM
- Een geoptimaliseerde van Dustin Diaz: maakt gebruik van getElementsByTagName
- Native implementatie: functie geïmplementeerd in de browser
- Implementatie met opzoektabel
Voor elk van deze methoden worden elementen op class opgezocht in een groot document (288kb). De resultaten van de test zijn opgenomen in tabel 1 in figuur 2. Uit deze resultaten blijkt dat de eerste twee methoden een stuk trager zijn dan de native methode, of de opzoektabel. De opzoektabel is in Firefox zelfs iets sneller dan de nativemethode, wat aangeeft dat er nog ruimte is voor optimalisatie in de implementatie van Firefox. Internet Explorer 6 en 7 hebben geen native ondersteuning voor getElementsByClassName en daar zijn geen test resultaten voor.
| Firefox 3 | IE 7 | IE 6 | |
|---|---|---|---|
| naief | 3927 | 10261 | 11641 |
| Diaz | 1139 | 1359 | 1411 |
| native | 25 | n.v.t. | n.v.t. |
| opzoektabel | 16 | 385 | 359 |

Figuur 2: Benodigde tijd in ms om tests uit te voeren (lager is beter)
De testbestanden en de resultaten zijn opgenomen als bijlage bij dit artikel.
Houdbaarheid van de oplossing
De methode is in de praktijk toegepast en heeft de prestaties van een webapplicatie flink opgeschroefd. Toch maakt de starheid dat het een methode is die beter alleen kan worden toegepast als andere methoden niet helpen. Hopelijk zal native ondersteuning voor getElementsByClassName in alle moderne browsers worden geïmplementeerd, waardoor zowel snel als flexibel met classes kan worden gewerkt. Idealiter worden dit soort optimalisaties weggestopt in bibliotheken, maar zolang de functie niet native is geimplementeerd, is een javascriptfunctie als fallback niet altijd snel genoeg.
[1] Een oud-collega kwam met de alternatieve term glorified enhancement, die de lading beter dekt.
[2] Het is goed mogelijk dat de code ook niet werkt in een HTML5-browser, de code is niet getest.
[3] Ik had de test van John Resig willen gebruiken, maar de testdata is niet beschikbaar. Een e-mail leverde geen reactie op.

Je mist een belangrijke referentie: http://www.whatwg.org/specs/web-apps/current-work/#dom-document-getelementsbyclassname
Het zit dus in de pipeline
Maar verder is er nog een extra optie, gewoon getElementsByName, die doet het al in alle browsers
Rikkert June 11, 2009 21:35
Goede oplossing met de names, daar had ik nog niet aan gedacht. Die oplossing heeft wel als nadeel dat je niet meerdere annotaties op een element kan zetten, zoals dat met het class attribuut wel kan. Als het name-attribuut al wordt gebruikt in een formulier, kun je er niet nog een waarde aan te toekennen voor de klasse waaronder het element valt.
Bijvoorbeeld:
[input type="text" name="home"/]
[input type="text" name="mobile"/]
Waarbij je aan beide inputs de annotatie ‘phone-number’ zou willen toevoegen voor een specifieke interactie.
Lennaert June 12, 2009 8:45
Inderdaad, maar in dat specifieke geval kan je beide inputs beter een id geven en daarmee werken. Eigenlijk heb je getElementsByClassName maar heel zelden nodig, maw: er zijn maar weinig cases waar het lonend is. Als je toch een opzoektabel moet maken, kan je het misschien maar beter helemaal overslaan als je maar twee elementen hebt.
Wat ook vaak voorkomt is dat alle elementen die je wilt hebben children zijn van een bepaalde container. In dat geval kan je nog sneller over de childnodes loopen.
Mijn advies: denk eerst goed na of een andere manier niet sneller is, bekijk vervolgens bovenstaande opties.
Rikkert June 12, 2009 9:55
Adviseer je geen gebruik te maken van classes, maar in plaats daarvan van ids of adviseer je geen gebruik te maken van een opzoektabel?
Het werken met een opzoektabel maakt het ontwikkelen inderdaad lastig omdat er een extra generatiestap tussenzit als de html wordt bijgewerkt, maar hopelijk heeft Internet Explorer straks ook getElementsByClassName en kan het raadplegen opzoektabel daarmee eenvoudig vervangen door de native functie.
Lennaert June 12, 2009 11:32
Ik adviseer ten eerste classes en ids te gebruiken waarvoor ze bedoeld zijn, ze hebben een verschillende semantische functie, je kan ze dus niet naar believen door elkaar gooien.
Vervolgens adviseer ik te doen wat het snelste werkt. In het geval dat je maar een gelimiteerde hoeveelheid terugverwacht (zeg minder dan 10 elementen), maakt de snelheid allemaal niet zo heel veel meer uit en adviseer ik gewoon te doen wat het makkelijkst voorhanden is (bijvoorbeeld in de ervaring van de devver of een gebruikte library)
Rikkert Koppes June 22, 2009 12:06
Really good work about this website was done. Keep trying more – thanks!
Yahoouj February 23, 2010 6:23