Wednesday, September 21, 2011

WPO - DOM und CSS

In den letzen Monaten wanderte der Fokus in der Web Performance Optimierung von der Zeit, die zum Laden einer Seite benötigt wurde, zunehmend auf die Geschwindigkeit, in der das Rendering einer Seite und die anschließende Manipulation der Seite durchgeführt werden kann.

Der letzte Blog-Beitrag konzentrierte sich auf JavaScript, wohingegen sich dieser Blog-Beitrag mit dem DOM und CSS beschäftigt.

DOM

Die Verschachtelung des DOMs sollte nicht zu tief werden, genauer: Eine Schachtelungstiefe von mehr als 15 sollte unbedingt vermieden werden.

Elemente sollten über CSS gestylt werden. Direkt in Elementen sollten Style-Attribute nur in Ausnahmefällen (z.B. zur individuellen Platzierung) verwendet werden.

Leere DOM-Nodes sollten vermieden werden. Dies gilt auch für Spacer- oder Clear-DIVs.

Auch auf sonstige überflüssige Elemente (z.B. Grafiken für gerundete Ecken) sollte nach Möglichkeit (d.h. wenn dies der Style-Guide erlaubt) verzichtet werden.

Die Komplexität des DOMs lässt sich mit dem Bookmarklet DOM-Monster von Thomas Fuchs auch auf fremden Seiten überprüfen.

Benutze Hardware-Beschleunigung

Moderne Browser (IE9, Mozilla und Webkit) bieten hardwarebeschleunigtes Rendering. Dieses macht sich allerdings erst bei der Berechnung von Animationen (Ein- und Ausblenden von Dialogen, Bewegen von Infoboxen über den Bildschirm) wirklich bemerkbar.

Hardwarebeschleunigung steht allerdings (wenn man von Canvas und WebGL absieht) nur CSS, nicht aber JavaScript zur Verfügung. Darum sollten Animationen stets per CSS mit Transitions erfolgen. JavaScript sollte nur als Fallback eingesetzt werden.

#transition_animated { -moz-transition: all 5s ease-out; -o-transition: all 5s ease-out; -webkit-transition: all 5s ease-out; transition: all 5s ease-out; }

Nutze CSS-Features durchgänging

Effekte wie Schatten oder gerundete Ecken können und sollten mit CSS realisiert werden. Für Browser, die dies nicht unterstützen, wird dann auf eine Darstellung ohne diese Effekte zurückgegriffen. Einige CSS-Features kann man in alten IE-Versionen durch CSS3-Pie nachrüsten. Allerdings wirkt sich CSS3-PIE negativ auf die Laufzeit aus.

CSS-Selektoren

Ein Browser wertet CSS-Selektoren von rechts nach links aus. Eine Auswertung von rechts nach links ist nämlich technisch einfacher und performanter zu implementieren, da mit diesem Ansatz lediglich Listen gefiltert (reduziert) werden müssen. D.h., zunächst werden im Dokument alle Elemente ermittelt, die im rechten Teil des CSS-Selektors stehen, dann wird durch die Liste der gefundenen Elemente iteriert. Auf jedes vorher ermittelte Element wird dann der nächste Selektor ausgewertet. Die Liste wird um diesen Selektor reduziert.

Hierzu ein Negativbeispiel aus einem Stylesheet, das so wirklich existierte:

div#page_navigation #navbar_01 ul.navbar_02 li.act_childs a

• a – Es werden alle Links einer Seite in einer Liste gesammelt. Diese kann sehr lang werden, denn die Seite kann sehr viele Links enthalten.
• li.act_childs – Es werden von allen Elementen in der Liste die Parent-Nodes ermittelt, um zu prüfen, ob diese ein Listen-Element mit der Klasse act_childs enthalten. Die Liste wird anhand dieses Kriterium reduziert.
• ul.navbar_02 – Es werden von allen Elementen in der Liste die Parent-Nodes ermittelt, um zu prüfen, ob diese eine ungeordnete Liste mit der Klasse navbar_02 enhalten. Die Liste wird anhand dieses Kriterium reduziert.
• #navbar_01 – Es werden von allen Elementen in der Liste die Parent-Nodes ermittelt, um zu prüfen, ob ein Element die ID navbar_01 hat. Eine ID muss auf einer HTML-Seite eindeutig sein. Die Liste wird anhand dieses Kriterium reduziert.
• div#page_navigation – Obwohl im vorherigen Selektor bereits eine eindeutige ID angegeben wurde, werden von allen Elementen in der Liste die Parent-Nodes ermittelt, um zu prüfen, ob diese ein DIV mit der ID page_navigation enhalten. Ein ID-Selektor ist bereits eindeutig. Eine zusätzliche Überprüfung, ob dies ein DIV ist, ist überflüssig. Die Liste wird durch dieses Kriterium nicht wirklich reduziert, denn der vorhergehende ID-Selektor war bereits eindeutig.

Daher gelten einige Regeln für effektive CSS-Selektoren:

Verwende effektive Selektoren

IDs sind die effektivsten Selektoren, gefolgt von Klassen-, Tags- und Universal-Selektoren.

#main_navigation /* ID */
.main_navigation /* Class */
ul li a /* Tag */
li a [title='Zurück zur Startseite'] /* Universal */

Man sollte stets den schnellsten geeigneten Selektor möglichst weit rechts verwenden und den HTML-Code entsprechend gestalten. Selektoren auf Tags und Universal-Selektoren sollten komplett vermieden werden.

Ebenso sollten Descendant-Selektoren vermieden werden, die am langsamsten sind (siehe oben!). Oft lassen sich Descendant-Selektoren durch Child-Selektoren beschleunigen. Child-Selektoren sind zwar auch nicht schnell, aber doch schneller als Descendant-Selektoren, da von einem Element nicht potentiell alle Vorfahren, sondern lediglich das Elternelement ermittelt werden muss.

ul.navbar_02 li.act_childs /* bad */
ul.navbar_02 > li.act_childs /* still bad but better */

Überqualifiziere Selektoren nicht

IDs sind bereits eindeutig. Auch Klassen sollten nur für einen Tag definiert sein. Daher sind überqualifizierte Selektoren wie div#main_navigation oder li .act_childs überflüssig. Es genügt, die Elemente über den sehr schnellen ID-Selektor oder den Class-Selektor zu filtern.

Halte die Selektor-Kette kurz

Je länger die Selektor-Kette, desto mehr Operationen muss der Browser durchführen, um ein Element zu finden. Daher sollte die Kette der Selektoren kurz gehalten werden; überflüssige sollten ganz vermieden werden. Wenn sich eine Selektor-Kette nicht vermeiden lässt, dann sollte sie möglichst früh fehlschlagen, damit Elemente nicht überflüssig oft evaluiert werden müssen, bevor sie herausgefiltert werden.

Benutze Kaskadierung

Oft lässt sich eine lange Selektor-Kette durch Kaskadierung vermeiden. CSS kaskadiert Style-Informationen hin zu den Sub-Elementen. Daher lässt sich ein Style oft schon an einem Vater-Element notieren, damit er im Kinder-Element angewendet wird.

Lange Selektoren trifft man oft an: #navbar_01 ul.navbar_02 li a { font: „Arial“}

Dieser lange Selektor könnte durch die Verwendung eines kurzen Selektors minimiert werden.

#navbar_01 { font: „Arial“}

Dieser Blog-Beitrag sollte gezeigt haben, dass nicht nur dynamische Inhalte und Download-Größen sondern auch Struktur und Styling mitverantwortlich für die Performance einer Website sein können.

Dies ist ein Cross-Post vom Holisticon-Blog.

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.