Tuesday, September 13, 2011

WPO - JavaScript und das DOM

Web Performance Optimierung konzentrierte sich in den letzten Jahren vor allem auf die Optimierung der Ladezeit einer Seite. Dazu haben sich Best Practices durchgesetzt, die bereits in einem Artikel in diesem Blog beschrieben wurden.

In jüngster Zeit wandert der Fokus jedoch immer mehr auf die Optimierung einer geladenen Seite, also auf die Zeit, die ein Browser für das Rendering und die Manipulation des DOMs benötigt.

In diesem Blog-Beitrag werden ich mich auf JavaScript konzentrieren.

Document.write

Die Verwendung von document.write ist generell – auch für JavaScripte von Dritten – untersagt. Es sind stattdessen DOM-Operationen (auch innerHtml) zu verwenden. Dies dient nicht nur der der Performance, sondern auch der Robustheit.

Inline-JavaScript

Inline-JavaScript sollte vermieden werden. Wenn dies nicht geht, sollten sie sich am Ende der Seite befinden. Das gilt auch für Event-Listener, die direkt in HTML-Elemente geschrieben werden. Diese sollten eigentlich erst nach dem Laden der Seite gebunden werden. Es sollten zudem so wenige JavaScript-Blöcke wie möglich verwendet werden, da jeder Block zu einer kurzen Verzögerung im Rendering führt.

JavaScript-Blöcke

Jeder JavaScript-Block wird isoliert abgearbeitet. Wenn in einem solchen ein Fehler auftritt, beeinflusst dies andere JavaScript-Blöcke nicht. Daher sollten Scripte, die mit Scripten von Dritten (Tracking, Targeting etc.) interagieren, in einem eigenen Block laufen. Dies widerspricht zwar der Anforderung, möglichst wenige Inline-JavaScripte einzubinden, sorgt aber für Robustheit der Seite. Um Performance-Einbußen zu minimieren, sollten diese Blöcke ausschließlich am Seitenende eingesetzt werden.

Selektoren

Selektoren, die z.B. in jQuery verwendet werden, sollten möglichst performant sein.

Nach Möglichkeit sollten nur Selektoren verwendet werden, die moderne Browser nativ implementieren können. Ob ein Selektor in der nativen Implementierung eines Browser funktioniert, kann man über die Funktion document.querySelectorAll in der Firebug-, Safari-, Internet Explorer- oder Chrome-Konsole testen (z.B. document.querySelectorAll(“.container .p–heading-1″)).

Zum Beispiel implementieren nicht alle Browser die Funktion document.getElementsByClassName. Ein Class-Selektor müsste also von einer Library wie jQuery implementiert werden. Wenn man einen Class-Selektor verwendet, dann muss jQuery alle Elemente der Seite (oder des Bereichs) über einen *-Selektor in eine Liste sammeln und jedes Element prüfen, ob seine Klasse der Klasse des Selektors entspricht. Dies kann auf einer Seite mit sehr vielen Elementen lange dauern. Statt Class-Selektoren sollte man also möglichst ID-Selektoren verwenden. Wenn ein ID-Selektor nicht möglich ist, kann man Events auch per Event Delegation (siehe unten!) an ein Vorfahren-Element binden, dass sich per ID referenzieren lässt.

Minimiere DOM-Manipulationen

JavaScript-Engines werden immer schneller. Dies gilt aber nicht unbedingt für den Zugriff auf das DOM. Manipulationen hieran sind teuer, weswegen sie minimiert werden sollten. Auf DOM-Manipulationen in Schleifen sollte nach Möglichkeit völlig verzichtet werden.

Cache DOM-Nodes und Attribute

Das Ermitteln von DOM-Elementen und -Attributen kostet Zeit. Daher sollten Elemente und Attribute einmalig ermittelt und dann in Variablen gecacht werden. Wenn sich das DOM verändert, so verändert sich auch automatisch das DOM-Element, das bereits ermittelt wurde – es besteht keine Notwendigkeit, erneut das Element im DOM zu suchen und auszuwerten. Module, die Objekte kapseln, können auch zur Zwischenspeicherung der DOM-Elemente genutzt werden.

Minimiere Redraws und Reflows

Jede Änderung im DOM führt zu einer Neuberechnung und einem Rendering der Seite. Dieser Redraw findet immer nach Events bzw. nach dem Beenden von JavaScript-Callbacks, die durch diese Events ausgelöst wurden, statt.

Änderungen sollten kumulativ erfolgen. Statt aus dem DOM zu lesen, in das DOM zu schreiben, erneut aus dem DOM zu lesen und wieder in das DOM zu schreiben, sollten Lese- und Schreiboperationen gebündelt erfolgen, so dass diese Operationen in einem einzigen Redraw bzw. Reflow erfolgen.

Das Document-Ready-Event

Im Document-Ready-Event sollte so wenig wie möglich getan werden. Lediglich Event-Listener dürfen an Elemente gebunden werden. Diese Elemente sollten nach Möglichkeit per ID referenziert werden. Wenn dies nicht möglich ist, bietet sich Event Delegation (siehe unten!) an.

Gänzlich verzichtet werden sollte auf DOM-Manipulationen, die schon beim Laden der Seite stattfinden (wie das Erzeugen von DIVs auf Reserve oder das Setzen von Attributen aufgrund von CSS-Klassen). Das DOM sollte bereits auf dem Server statt erst im Browser manipuliert werden.

Lazy Initialisation

Berechnungen und Bindings beim Document-Ready-Event sollten minimiert werden (siehe oben!), sondern erst durchgeführt werden, wenn sie benötigt werden (z.B. nach einen Klick-Event). Natürlich ist im Einzelfall abzuwägen, ob Lazy Initialisation einen Vorteil bietet. Eine Lazy Initialisation bei einem MouseOver-Event könnte als störend (verzögert) empfunden werden, während sie nach einem Klick-Event vom User nicht bemerkt wird.

Event Delegation

Es kann nach dem Laden der Seite lange dauern, bis alle Event Handler an DOM-Elemente gebunden sind. Um diese Zeit zu minimieren, bietet sich Event Delegation an, die der der Lazy Initalisation ähnelt. Man registriert ein Event (z.B. ein Klick-Event) an einem umschließenden Bereich, der sich z.B. über eine ID referenzieren lässt. Die Auflösung auf das einzelne geklickte Element findet dann erst nach dem Klick-Event statt. Die Zeit, die beim Laden der Seite eingespart wird, tritt dann also bei jedem einzelnen Event auf. Daher ist im Einzelfall abzuwägen, ob Event Delegation einen Vorteil bietet. Eine Event Delegation bei einem MouseOver-Event könnte als störend empfunden werden, während eine Event Delegation nach einem Klick-Event vom User nicht bemerkt wird.

Dies ist ein Cross-Post vom Holisticon-Blog und von Ajaxer.