CSSOM, der heimliche Zwilling des DOMs

von Henry Zeitler

Das Document Object Model, kurz DOM, ist ein guter alter Bekannter des Developers. Es ist eine Ansammlung aus vielen Knoten und Objekten, die die Struktur und Inhalte der HTML-Elemente abbilden. Doch das DOM hat einen unbekannten Zwilling: das Cascading Style Sheet Object Model.

Dieser Artikel erschien am 12.Dezember 2015 im Webkrauts Advents-Kalender.

Eine Zusammenstellung verschiedener Objekte, die über ein Interface (API) manipuliert werden können, wird Object Model genannt. Das DOM beschreibt in diesem Zusammenhang eine Sammlung von Objekten, die in einem Browser dargestellt werden. Das API des DOMs kann mit Hilfe einer Vielzahl von JavaScript-Methoden angesprochen und manipuliert werden.

Es werden aber auch Stylesheets über ein eigenes Object Model auf die einzelnen Knoten und Objekte im DOM verteilt. Das Prinzip des CSSOM ist eigentlich gar nicht so neu, und erste Spezifikationen für eine API gibt es bereits seit der Document Object Model (DOM) Level 2 Style Specification aus dem Jahr 2000. Die durch fortlaufende Neuerungen wie CSS3 ständig wachsende Anzahl von Interfaces wurde im Oktober 2015 erneut in einem W3C Editor‘s Draft zusammengefasst, dem CSS Object Model (CSSOM).

Das CSSOM spielt eine bedeutende Rolle während des Renderns einer Seite im Browser und besitzt eine eigene API. Wer also die Arbeitsweise eines Browsers versteht ist in der Lage, die Performance einer Webseite drastisch zu verbessern.

DOM und CSSOM – auf die Zusammenarbeit kommt es an

Die Arbeitsschritte des Browsers
Die Arbeitsschritte des Browsers

Wenn dem Besucher eine Internetseite angezeigt wird, hat der Browser bereits eine Menge Arbeit hinter sich. Die Vorgehensweise des Browsers bei der Erstellung des DOMs und die Verteilung der Styles während des Renderns einer Seite zeigt ganz gut auf, was den Best-Practices bezüglich CSS, JS und Performance im Frontend zu Grunde liegt.
Die wichtigsten Schritte kurz erklärt. Das CSSOM wird dabei genauso wie das DOM bearbeitet:

Das Parsing

Über die im Browser eingegebene Adresse (URI) werden die angeforderten Daten abgerufen. Diese werden in Bytes ausgeliefert und vom Rendering Modul auf Grund der mitgelieferten Dateicodierung (z. B. UTF-8) in einzelne Zeichen umgewandelt. Anhand der generierten Zeichenfolgen werden danach die von der W3C spezifizierten HTML-Tags (Start-Tags, End-Tags, Attribute) als Tokens aufgeschlüsselt. Dieser Vorgang wird als Tokenisierung bezeichnet vom englischen Token (Zeichen).
Bevor das OM zusammengesetzt werden kann, müssen diese Tokens in Objekte umgewandelt werden. Alle Objekte im HTML besitzen festgelegte Eigenschaften und unterliegen speziellen Regeln (W3C-Spezifikationen). Die Vergabe dieser Eigenschaften und Regeln erfolgt auf Basis einer lexikalischen Analyse und einer Syntaxanalyse durch den Parser und bastelt aus dem einfachen Token letztlich ein HTML-Objekt. Dieser Vorgang wird als Lexing bezeichnet.
Über die Beziehungen der Objekte wird schließlich die Ausgabestruktur oder auch der Parsing-Baum konstruiert. Dafür müssen deren Verschachtelungen und Hierarchien erkannt sowie definiert und daraufhin das DOM generiert werden. Wohlgeformter und valider Quellcode beschleunigen den Prozess, da HTML-Parser dazu neigen, das HTML so zu optimieren, dass es letztlich dargestellt werden kann.

Der komplette, oben eher oberflächlich beschriebene Vorgang ist das HTML Parsing. Er muss jedes Mal, wenn der Browser Markup erstellt, hinzufügt oder entfernt, durchlaufen werden.

So wird das HTML generiert. Um aber die einzelnen Objekte des DOM im Browserfenster zu positionieren, ihre Darstellung festzulegen und die Inhalte konsumierbar zu machen, bedarf es zudem eines Stylesheets. Dieses kann ein Browser-Stylesheet (User-Agent-Stylesheets), Autoren-Stylesheet (das Stylesheet, das mit der Website ausgeliefert wird) oder Nutzer-Stylesheet (z. B. die Browsereinstellungen des Besuchers) sein. Autoren-Stylesheets können entweder aus einer externen Datei, im Kopf einer Webseite oder als Inline-Styles eingelesen werden.

Info: Kaskadenreihenfolge gemäß CSS2-Spezifikation

  1. Browserdeklarationen
  2. Normale Nutzerdeklarationen
  3. Normale Autorendeklarationen
  4. Wichtige Autorendeklarationen
  5. Wichtige Nutzerdeklarationen

Stößt der Parser während des HTML Parsings auf ein Style- oder ein Script-Tag wird der Vorgang unterbrochen. Das Stylesheet wird geladen bzw. das Skript erst einmal ausgeführt. Das liegt in der grundsätzlich synchronen Natur des Webs. Und da ein Skript oftmals die Stileigenschaften von DOM-Elementen anfragt, muss auch rein prophylaktisch jedes Stylesheet erst konsumiert werden, um Fehlern in den Skripten durch falsche Rückgabewerte vorzubeugen.

Auch Stylesheets kommen im Browser zuerst als Bytes an und müssen in Zeichen, danach in Token, Knoten und schließlich in die CSSOM-Baumstruktur konvertiert werden. Dieser Vorgang ist der Erstellung des DOM wie oben beschrieben sehr ähnlich.

Rendering

Bei der Konstruktion des CSSOM und der Vergabe der CSS-Regeln geht der Browser folgendermaßen vor: Zuerst werden auf sämtliche Knoten im DOM die allgemeinsten Regeln angewendet. Das sind zum einen die Browser-Stylesheets und Nutzerstylesheets zum anderen allgemeingültige Autoren-Styles, die sich auf Elemente wie <html> oder <body> beziehen und von hier aus auf alle unteren Knoten vererben. Danach werden die Styles mit den spezifischeren Regeln rekursiv bearbeitet und zugewiesen.
Hat der Browser alle Regeln abgearbeitet und vergeben, werden die für ein Element relevanten Stildaten im CSSOM gespeichert.

Die Zusammenarbeit von DOM und CSSOM. Nicht sichtbare Elemente werden aus dem Render Tree entfernt.
Die Zusammenarbeit von DOM und CSSOM. Nicht sichtbare Elemente werden aus dem Render Tree entfernt.

Das DOM und das CSSOM zusammen bilden den Render-Tree. Hier werden alle sichtbaren Elemente des DOM mit den entsprechenden Style-Informationen des CSSOM versorgt und das gesamte Konstrukt gespeichert.

Das Layouting (Reflow)

Wie bereits erwähnt beinhaltet der Render-Tree jetzt alle sichtbaren Elemente. Allerdings fehlen noch die Angaben für die Positionierung innerhalb des Viewports und zur Berechnung der Größe der einzelnen Elemente. Das passiert während der Layouting-Phase.

Das Ergebnis der Layouting-Phase nach der Berechnung ist das bekannte Box-Model. Relative Maßangaben wie Prozent, rem und em werden in Pixel konvertiert und die exakten Positionen und Ausmaße der Elemente festgelegt. Das HTML-Gerüst steht jetzt virtuell – es muss lediglich noch angemalt und in Form von Pixeln auf den Bildschirm gebracht werden.

Das Painting

Der Painting-Prozess bringt die Pixel auf den Bildschirm und wird in einem Stapelkontext (Stacking Context) vollzogen. Das bedeutet, es werden verschiedene Ebenen nacheinander abgearbeitet – beginnend mit der untersten bis hin zur obersten. Die Painting-Reihenfolge für einen typischen Block-Renderer sieht folgendermaßen aus:

  1. Hintergrundfarbe
  2. Hintergrundbild
  3. Rahmen
  4. Untergeordnete Elemente
  5. Umriss

Das CSSOM und die Browser-Performance

Der oben beschriebene Prozess, den ein Browser durchlaufen muss, ist – wie gesagt – stark gekürzt. In Wirklichkeit gibt es eine Menge mehr Arbeitsabläufe und Berechnungen, die der Darstellung einer Webseite vorausgehen. Eines ist jedoch klar ersichtlich: ein Browser erledigt Schwerstarbeit in wenigen Millisekunden.
Wer die Arbeitsweise des Browsers und das CSSOM versteht, kann hier einiges optimieren. Die folgenden Methoden kennen bereits die meisten Developer, aber sie werden unter diesem Aspekt wesentlich nachvollziehbarer.

Das CSSOM blockiert den Render-Vorgang

Der Browser unterbricht den Render-Vorgang und wartet, bis das gesamte CSS geladen wurde. Andernfalls würde die Seite unformatiert dargestellt werden und erst nach Erhalt und Anwendung der Styles zu der formatierten Ansicht wechseln. Ein sogenannter FOUC (Flash Of Unstyled Content) vor jeder Seitenansicht wäre die Folge.

Um das Blockieren des Render-Vorgangs zu minimieren, werden Stylesheets zusammengefasst (konkateniert), um externe Zugriffe zu reduzieren. Jede Anfrage (Request) an den Server kostet Zeit, blockiert das Rendering und sollte darum vermieden werden.
Die Wartezeit, bis die Seite angezeigt wird, wird deutlich verringert, wenn Styles, die sich auf die sichtbaren Bereiche einer Webseite beziehen, direkt in den Head der Datei als Inline-Styles eingebunden und die externen Styles asynchron geladen werden (Critical CSS).

Javascript blockiert, bis das CSSOM fertig ist

Auch Skripte müssen zuerst vollständig eingelesen und ausgeführt werden, bis das Rendering weitergehen kann. Wie oben bereits beschrieben greift Javascript häufig auf das DOM/CSSOM (sprich: den Render-Tree) zurück, um z. B. Werte anzufragen oder zu manipulieren. Wenn diese Werte nicht verfügbar sind, kann es zu Fehlern bei der weiteren Bearbeitung der Seite kommen.

Deswegen sollten Skripte erst möglichst weit unten im Markup eingebunden und so spät wie möglich – nach dem CSS – aufgerufen werden. Skripte mit einem src-Attribut können auch über die async- oder defer-Attribute asynchron geladen werden. Wobei das defer-Attribut die Ausführung des Skripts verzögert, bis die Seite vollständig geparst wurde, und das async-Attribut das Skript zum nächstmöglichen Zeitpunkt ausführt ohne das Parsing zu blocken.

Das CSSOM wird jedes Mal neu gebaut, wenn eine Seite geladen wird

Auch wenn das CSS im Cache des Browsers abgelegt und der Zugriff ab diesem Moment eingespart wird, muss das CSSOM bei einem Seiten-Refresh oder dem Besuch einer Folgeseite neu berechnet werden.

Sauberes, perfomantes und minimiertes CSS helfen dem Render-Modul, diesen Vorgang jedes Mal schnellstmöglich abzuschließen. Zu den Best-Practices gehören z. B. flache Spezifizierungen und das D.R.Y-Paradigma (Don’t Repeat Yourself).

Die API des CSSOM

Wie anfangs bereits erwähnt, verfügt das CSSOM über eine ganze Reihe von Interfaces und viele davon benutzt der Developer wahrscheinlich täglich. Über die Interfaces lassen sich mittels Jacascript CSS-Werte und Eigenschaften abfragen und/oder dynamisch anpassen. Angefangen hat das CSSOM eigentlich bereits mit CSS 2 (DOM Level 2 Style) in der entsprechenden W3C Recommendation. Mittlerweile gibt es eine eigene Spezifikation, die im W3C Working Draft CSSOM beschrieben wird.

Die Methoden der API arbeiten also mit Objekten – der Developer hat es ja mit einem Object-Model zu tun. Objekte können ineinander verschachtelt sein und folgen dann einer eindeutigen Hierarchie. Z.B. gibt es seit CSS2 folgende Objekt-Hierarchie zur Abfrage von Styles:

  1. StyleSheetList
    Gibt eine Liste aller Stylesheets zurück, sie besteht aus einzelnen CSSStyeSheets.
  2. CSSStyleSheet
    Ein einzelnes Stylesheet aus der obigen Liste.
  3. CSSRuleList
    Eine Liste aller Regeln (CSSStyleRule) eines Styleheets aus Punkt 2.
  4. CSSStyleRule
    Ein Objekt mit einer einfachen Style-Regel.

Als JS-Code sieht das so aus:

  1. var getRule = document.styleSheets[i].cssRules[i];

Auf die Startseite von webkrauts.de angewendet, werden dann folgende Objekte zurückgeliefert:

Darstellung des CSSOM in den DevTools
Das Objekt zu document.styleSheets[0]
Darstellung des CSSOM in den DevTools mit aufgeklappten CSSRuleList
Die Regeln verbergen sich unter cssRules
Darstellung des CSSOM in den DevTools mit einzelnem CSSStyleRule
Also kann eine direkte Abfrage z. B. über document.styleSheets[0].cssRules[1] erfolgen

Über die APIs lassen sich Stylesheets nicht nur abfragen, sondern natürlich auch verändern, löschen und hinzufügen. Eine umfassende Darstellung der Möglichkeiten würde aber den Rahmen dieses Artikels sprengen. Wer sich mehr in das Thema vertiefen möchte sollte sich unbedingt die folgenden Links ansehen.

Quellen und weiterführende Links: