Webdesign mit CSS, HTML und Javascript
Stil mit {stil}
![]() Praxistests aktueller Kameras, Bildbearbeitung und Bildgestaltung foto.5lux.de Online-Magazin für Fotografie und Bildbearbeitung |
Aug 2007
Javascript Effizienz • Schnellere und kürzere Scripte
Inline-Skripte
Inline-Skripte wie <body onload="javascript: start()"> und <input onclick="" ... /> verlangsamen den Aufbau einer Seite, da der Browser annehmen muss, dass ein Inline-Skript die Struktur der Seite verändert.
Arrays
Ein Array kann man wie folgt aufbauen:
var tage = new Array(7); tage[0] = "Montag"; tage[1] = "Dienstag"; tage[2] = "Mittwoch"; ...
Schneller geht es so:
var tage = new Array ("Montag",
"Dienstag",
"Mittwoch",
...
"Sonntag");
Schnelle Schleifen
Schleifen sind das A & O jeder Anwendung und mit optimierten Bedingungen lassen sie sich durchaus schneller gestalten.
for (var i=0; i<document.getElementsByTagName('tr').length; i++ ) {
document.getElementsByTagName('tr')[i].className = 'newclass';
document.getElementsByTagName('tr')[i].style.color = 'red';
...
}
Schon besser und besser lesbar
var rows = document.getElementsByTagName('tr');
for (var i=0; i<rows.length; i++ ) {
rows[i].className = 'newclass';
rows[i].style.color = 'red';
...
}
Keine dieser beiden Varianten ist wirklich effizient. getElementsByTagName gibt kein statisches Array, sondern ein dynamisches Objekt zurück. Jedes Mal, wenn die Bedingung geprüft wird, muss der Browser auf das Objekt zugreifen und die Anzahl der referenzierten Objekte berechnen, um die Eigenschaft length zurückzugeben.
Ähnlich sieht es mit dem verwendeten Index aus. Das Objekt muss bei jedem Lauf durch die for-Schleife drei Mal berechnet werden. Dieser Code sind besser, wobei meistens die erste Variante die bessere ist:
var rows = document.getElementsByTagName('tr');
for (var i=0, row; row=rows[i]; i++ ) {
row.className = 'newclass';
row.style.color = 'red';
...
}
var rows = document.getElementsByTagName('tr');
for (var i=rows.length-1; i>-1; i-- ) {
var row = rows[i];
row.className = 'newclass';
row.style.color = 'red';
...
}
Abkürzungen (Short Circuit)
Der Short Circuit-Operator (&&) hilft bei der Optimierung bedingter Anweisungen und erlaubt die Ausführung teurer Operationen nachdem weniger teure Bedinungen bereits erfolgreich geprüft wurden: Die zweite Bedingung wird erst evaluiert, wenn die erste Bedingung erfüllt ist. Also setzt man die aufwendigere Prüfung nach hinten.
Der ||-Operator arbeitet ähnlich und evaluiert die zweite Bedingung nur wenn die erste Bedingung nicht erfüllt ist. Wenn zwei Bedingungen vorliegen, aber nur eine Bedingung erfüllt sein muss, damit das Script fortgeführt werden kann, wird die weniger aufwendige Bedingung nach vorn gesetzt, so dass die zweite Bedingung nur geprüft werden muss, wenn die erste Bedingung nicht zutrifft.
Zugriff auf Elemente
Das DOM stellt viele Methoden für den Zugriff auf Elemente zur Verfügung und man läßt sich schnell auf eine ausgiebige Benutzung von childNodes, siblings, parentNodes und tagNames ein. Diese Technik ist sowohl unzuverläßig als auch langsam, insbesondere, wenn Elemente in das Dokument eingefügt und entfernt werden. Wo immer es möglich ist, sollte getElementById für den Zugriff auf ein Element benutzt werden, um so nah wie möglich an das Element zu gelangen. Außerdem sollte man immer in Betracht ziehen, dass Weißraum zwischen den Elementen zu einem childNode wird und muss dementsprechend immer getElementsByTagName benutzen, damit sicher gestellt ist, dass der Zugriff auf das korrekte Element erfolgt.
Immer wieder wird ein Zugriff auf mehrere Elemente benötigt, z.B. auf alle h-Überschriften in einem Dokument. Auf die h-Elemente können wir mit getElementsByTagName('*') zugreifen, aber das würde auf viel zu viele Elemente zugrifen und die Schleifen deutlichlich langsamer machen. Der Weg sollte nur eingeschlagen werden, wenn die header in der korrekten Reihenfolge benötigt werden.
var headers = document.getElementsByTagName('*');
for (var i=0, oElement; oElement=headers[i]; i++ ) {
if (oElement.tagName.match(/^h[1-6]$/i) {
...
}
}
Diese Technik ist besser:
for (var i=1; i<7; i++ ) {
var headers = document.getElementsByTagName('h'+i);
for (var j=0, oElement; oElement=headers[j]; j++ ) {
...
}
}
Den Wiederaufbau einer Seite minimieren
Jedes Mal, wenn ein Element in ein Dokument eingefügt wird, muss der Browser den Elementfluss der Seite neu aufbauen, Elemente neu positionieren und rendern. Je mehr eingefügt wird, desto mehr muss neu aufgebaut werden. Also reduziert man die Anzahl der Elemente, die hinzugefügt werden, damit der Browser nicht so häufig eine Neustrukturierung vornehmen muss.
Wird ein Element mit mehreren Kindern hinzugefügt, werden zuerst die Kinder in das Element eingefügt und dann erst das Element in das Dokument eingefügt, so dass der Browser mit einer einzigen Neustrukturierung auskommt. Wenn mehrere sibling-Elemente nicht als Kinder eines neuen Elements eingefügt werden, kann man ein document fragment benutzen, die Elemente dort unterbringen, und dann das document fragment in das Dokument einfügen. Die Elemente werden dann als siblings in einer einzigen Neustrukturierung untergebracht.
var foo = document.createDocumentFragment();
foo.appendChild(document.createElement('p'));
foo.firstChild.appendChild(document.createTextNode('Test'));
foo.lastChild.appendChild(document.createTextNode('Me'));
foo.firstChild.style.color = 'green';
document.body.appendChild(foo);
Das selbe gilt für den Text und Stile eines Elements: Zuerst wird werden Inhalte und Stile eingefügt und dann erst das Element in das Dokument gesetzt.
Mehrere Stile
So sieht das Einfügen mehrerer Stile immer wieder aus:
oElement.style.position = 'absolute'; oElement.style.top = '0px'; oElement.style.left = '0px'; ... etc ...
Das ist wäre megaout, da es den veralteten Ansatz des DHTMLs benutzt und richtig Perfomance kostet. Wir könnten das DOM nehmen und reguläres CSS in einer einzigen Neustrukturierung einfügen:
oElement.setAttribute('style','position:absolute; top:0px; left:0px;... ...');
Leider beherrscht IE zwar setAttribute, aber ausgerechnet setAttribute('style', …) nicht.
Noch effizienter ist es allerdings, gleich eine CSS-Klasse zu benutzen und die Klasse im Style Sheet zu vereinbaren.
oElement.setAttribute('class','myClass');
Allerdings braucht IE dafür eine Sonderlösung
oElement.setAttribute('className','myClass');
Für IE muss also className anstelle von class verwendet werden. Gott sei Dank verkraften alle Browser beide Aufrufe, so dass keine Objekterkennung erforderlich ist.
Anonyme Funktionen
Wir wollen die Hintergrundfarbe einer Tabellenreihe beim mouseover ändern und beim mouseout wieder zurücksetzen. Schreiben wir klassisch so:
if (document.getElementsByTagName) {
var rows = document.getElementsByTagName('tr');
for (var i=0, row; row=rows[i]; i++) {
row.mouseover = over;
row.mouseout = out;
}
}
function over()
{
this.setAttribute('style','background: silver');
}
function out()
{
this.setAttribute('style','background: blue');
}
Da die Funktionen over() und out() so einfach sind, können wir sie als anonyme Funktionen registrieren:
for (var i=0, row; row=rows[i]; i++) {
row.mouseover = function (event) {
this.setAttribute('style','background: silver');
}
row.mouseout = function (event) {
this.setAttribute('style','background: blue');
}
}
(Der Parameter event ist optional)
Das funktioniert genauso gut, hält aber den Code besser beisammen. Auch dieser Konstrukt ist OK:
obj.onclick = function (event) {
functionOne();
functionTwo();
}
String Matching
Um einen String nach einen bestimmten Substring zu durchsuchen gibt es zwei grundlegende Techniken. Die erste benutzt 'indexOf', um die Position des Substrings im String herauszufinden. Die zweite Technik ist 'match' oder eine entsprechende Methode, um ein Suchmuster mit einem regulären Ausdruck zu suchen. stringObjects.match ist schneller als stringObjekt.indexOf. Nur wenn die Suchmuster sehr einfach sind, sollte indexOf anstelle des regulären Ausdrucks verwendet werden. 'match' sollte ebenfalls vermieden werden, wenn der String sehr lang (10KB und mehr) ist.
Werden reguläre Ausdrücke mit vielen Wildcards benutzt, wird string matching ebenfalls deutlich langsamer. Die wiederholte Ersetzung von Substrings ist ebenfalls kostenintensiv.
Sinnvolle Wiederverwertung
Wenn das Suchmuster eines regulären Ausdrucks wiederholt verwendet wird, sollte es einmal erzeugt und als Variable gespeichert werden, damit der Browser die Suche optimieren kann. Im Beispiel werden die beiden regulären Ausdrücke getrennt behandelt und verschwenden Resourcen:
if( a == 1 && oNode.nodeValue.match(/^\s*extra.*free/g) ) {
//erzeugt die erste Kopie
} else if( a == 2 && oNode.nextSibling.nodeValue.match(/^\s*extra.*free/g) ) {
//erzeugt die zweite Kopie
}
Die folgenden Anweisungen arbeiten identisch, aber effzienter, da der Browser nur eine Kopie des regulären Ausdrucks anlegen muss:
var oExpr = /^\s*extra.*free/g;
if( a == 1 && oNode.nodeValue.match(oExpr) ) {
// Benutzt die vorhandene Variable
} else if( a == 2 && oNode.nextSibling.nodeValue.match(oExpr) ) {
// Benutzt ebenfalls die vorhandene Variable
}
Grundsätzlich muss der gespeicherte Ausdruck nicht gelöscht oder auf null gesetzt werden werden, wenn die Suche beendet ist. Die Script-Engine führt die Garbage Collection durch und der Löschvorgang ist eine unnötige zusätzliche Operation.
Wenn sie inline in einem Loop definiert wurde, wird jede Instanz eines regulären Ausdrucks nur einmal erzeugt und automatisch für den nächsten Zugriff in der Schleife gecacht. Wenn wir aber mehrere Instanzen des selben Ausdrucks innerhalb der Schleife erzeugen, wird jede separat erzeugt und gecacht, wie wir oben sehen.
for( var i = 0, oNode; oNode = oElement.childNodes[i]; i++ ) {
if( oNode.nodeValue.match(/^\s*extra.*free/g) ) {
// erzeugt des Ausdruck
// wird gecacht und beim nächsten Lauf durch die Schleife wiederverwertet
}
}
Das triff nicht auf die new RegExp-Syntax zu, die immer eine neue Kopie des Ausdrucks erzeugt und generell langsamer ist als die Erzeugung eines statischen Ausdrucks. In Schleifen sollte sie also immer vermieden werden, soweit dies möglich ist.
eval ist hinterhältig
Sowohl die Methode eval als auch verwandte Konstrukte wie new Function sind Verschwender.
Beim Laden einer Seite wird eine neue Instanz des Javascript-Interpreters gestartet und eine Skripting-Umgebung erzeugt (wird auch als „thread“ bezeichnet). Der Aufruf von eval startet einen weiteren Interpreter mit einer neuen Umgebung und importiert die Variablen der ersten Umgebung in die neue Umgebung. Wenn der eval-Aufruf abgewickelt ist, exportiert die Variablen zurück in ihre ursprüngliche Umgebung und sammelt den Müll. Obendrein kann der Code nicht für die Optimierung gecacht werden.
Nur auf das horchen, was gebraucht wird
Jedes Mal, wenn ein neuer Event Handler registriert wird, beginnt die Scripting-Engine mit dem Abhorchen und Feuern auf das Ereignis. Jeder weitere Event Handler legt also eine zusätzliche Last auf die Engine.
Unnötigen Code vermeiden
Wenn ein Script nur unter bestimmten Bedingungen laufen soll, werden die Bedingungen so klar wie möglich formuliert und das Script sofort gestopt, wenn die Bedingungen nicht erfüllt werden. Wenn das Skript innerhalb einer Funktion liegt – was bei dem größten Teil des JavaScript-Codes der Fall sein wird –, sollte return benutzt werden, um die Funktion sofort zu verlassen.
Um for- oder while-Anweisungen zu verlassen, wird break aufgerufen, continue überspringt umfangreiche Blöcke in for- und while-Anweisungen. Durchsucht ein Skript z.B. alle a-Tags eines Dokuments um ein href-Attribut mit einem Link auf eine CSS-Datei zu finden, kann die for-Anweisung sofort verlassen werden, wenn die CSS-Datei gefunden wurde. Wenn ein return nicht möglich ist, weil eine Funktion nach der for-Anweisung weiter ausgeführt werden soll, könnte eine Variable definiert werden und als zusätzliche Bedingung in der for-Anweisung untergebracht werden. Einfacher ist es allerdings, die for-Anweisung einfach abzubrechen:
document.addEventListener('load',function () {
var oLinks = document.getElementsByTagName('link');
for(var x=0, oLnk; oLnk = oLinks[x]; x++ ) {
if( oLnk.getAttribute('href').match(/\/old.css$/) ) {
oLnk.setAttribute('href','styles/new.css');
break;
}
}
},false);
Timer kosten kostbare Zeit
setInterval und setTimeout werden für Animationseffekte benutzt. Jede dieser Methoden erzeugt einen zusätzlich Thread für jeden angelegten Timeout und wenn der Browser mehrere Timeouts simultan ausführt, geht die Performance den Berg runter. Mit ein oder zwei Timern ist das noch kein Problem, aber sobald es fünf oder zehn werden, wird der Geschwindigkeitsverlust sichtbar. Dazu kommt noch, dass Timer zur eval-Familie der Methoden gehören, so dass sie gleich vielfach ineffizient sind.
Das Zusammenlegen mehrerer Timern in einen Timer kann den Einsatz von Timeouts optimieren.
Da ein Timer i.A. den Code in gleicher Weise evaluiert wie die Methode eval, sollte so wenig Code wie möglich den den evaluierten Anweisungen stehen. Statt den gesamten Code innerhalb der Timeout-Anweisung zu schreiben, packt man ihn besser in eine separate Funktion und ruft die Funktion aus der Timout-Anweisung aus. Dabei können wir eine direkte Funktionen-Refernz anstelle eines evaluierten Strings benutzen. Wie beim Beseitigen der Ineffizienz von eval vermeidet dieser Ansatz auch die Erzeugung globaler Variablen innerhalb des evaluierten Codes.
function doTimeoutStuff() {
for( var i = 0; i < 5; i++ ) {
document.getElementBy('collapse'+i).style.display = 'none';
}
}
setTimeout(doTimeoutStuff,2000);
Beim Einsatz einer Funktionsreferenz als erster Parameter von setTimeout oder setInterval können weitere Parameter den Timeout aufbauen.
function doTimeoutStuff(oFrom,oTo) {
for( var i = oFrom; i <= oTo; i++ ) {
document.getElementBy('collapse'+i).style.display = 'none';
}
}
setTimeout(doTimeoutStuff,2000,0,4);
