Blog

Web Performance-Optimierung

Performance-Optimierung

Bei einem Kunden wurde die Performance der Website optimiert. Inspiriert wurde diese Optimierung durch das Buch „High Performance Web Sites: 14 Steps to Faster-Loading Web Sites“ von Steve Souders. Anhand der Engineering-Tasks, die in diesem Buch beschrieben werden, wurden folgende Regeln aufgestellt und implementiert.

Rendering so früh wie möglich erlauben

Wenn ein Benutzer eine Seite aufruft, muss er warten, bis die Seite aufgebaut ist. Während er wartet, benötigt er ein visuelles Feedback, damit er sichergehen kann, dass die Seite funktioniert. Durch das Feedback erscheint für den Nutzer die Zeit, die er auf die Seite wartet, subjektiv kürzer. In Webseiten wird in der Regel kein Fortschrittsbalken eingeblendet, sondern die Seite selbst ist der Fortschrittsbalken. Statt dass der Nutzer vor einer weißen Seite sitzt, die sich nach der Wartezeit in einem einzigen Augenblick aufbaut, sollte sich die Seite im Browser nach und nach aufbauen. Um dies zu erreichen, sollte es dem Web-Browser technisch ermöglicht werden, heruntergeladene Inhalte so früh wie möglich darstellen zu können. Dazu benötigt er einfach möglichst früh alle nötigen Informationen zum Rendering der Seite. Außerdem sollte man – z.B. nach dem Rendering des Kopfbereichs – den HTTP-Output flushen. Eine weiße Seite erscheint (vor allem im Internet Explorer) trotzdem dann, wenn

  • man eine Seite in einem neuen Fenster öffnet,
  • der Browser während eines Reloads bewegt wird (z.B. minimiert und wiedergestellt wird),
  • eine Seite als Homepage (als erste Seite überhaupt) geladen wird.

Schlimmer als eine weiße Seite ist ein Flash of Unstyled Content (FUC). Dieser wird hervorgerufen, wenn Stylesheet-Informationen erst dann ausgewertet werden, wenn der Browser bereits mit der Darstellung des Contents begonnen hat.

Dimensionen von Grafiken und Bildern festlegen

Wenn man für Images (Bilder und Grafiken) die Höhe und Breite schon im Image-Tag vorgibt, kann der Browser mit der Berechnung der Seite schon beginnen, bevor die eigentlichen Binaries der Bilder heruntergeladen wurden.

CSS in den Seitenkopf einbauen

Stylesheets, die bereits im Dokumenten-Kopf (HEAD) eingebunden werden, ermöglichen es dem Browser, möglichst früh mit dem Rendering zu beginnen. Zudem wird durch das Einbinden in den Dokumenten-Kopf der FUC vermieden. CSS sollte immer über den Link-Tag eingebunden werden. @import-Regeln im CSS werden nämlich erst später, wenn das CSS bereits heruntergeladen wurde, evaluiert.

CSS vor JavaScript einbinden

Browser blockieren JavaScripte, bis alle CSS-Dateien geladen sind. Das liegt daran, dass JavaScripte Informationen aus Style-Attributen verwenden könnten. Darum muss der Browser vor der Ausführung von JavaScript warten, bis die CSS-Dateien geladen sind. Also sollte man, um eine Blockierung zu vermeiden, CSS-Dateien auch im HTML-Code vor JavaScript-Dateien referenzieren.

Reduziere die Anzahl der HTTP-Requests

Je weniger einzelne Ressourcen (Dateien) ein Browser laden muss, desto schneller können diese heruntergeladen werden, da der Over-Head durch HTTP bzw. durch TCP-Round-Trips entfällt. Außerdem gilt: Je kleiner diese Ressourcen sind, desto schneller werden sie übertragen.

Parallelisiere Downloads über verschiedene Hosts

Ein Browser kann pro Host nur eine bestimmte Anzahl an Verbindungen öffnen. Eine Übersicht, welcher Browser wie viele Verbindungen gleichzeitig öffnen kann, bietet die Website browserscope.org. Der Internet Explorer 6 (IE6) kann z.B. nur zwei Verbindungen pro Host öffnen. Wenn man die Ressourcen einer Site auf mehrere Hosts verteilt, dann kann der Browser eine größere Anzahl an Verbindungen verwenden. Für statische Inhalte wurden also zwei (virtuelle) Static-Hosts angelegt, über die diese Inhalte ausgeliefert werden.

Reduziere DNS-Lookups

Allerdings lassen sich nicht beliebig viele Host-Namen verwenden. Pro Host-Name ist nämlich ein DNS-Lookup notwendig. Firefox speichert DNS-Lookups nur für eine Minute zwischen. Wenn man also zu viele Hosts für den parallelen Download verwendet, wird der Geschwindigkeitsvorteil, der durch verschiedene Hosts erreicht wird, durch die DNS-Lookups wieder aufgehoben. Es sollten also pro Site nicht mehr als 2-4 Hosts zur Parallelisierung verwendet werden. Das Einbinden von Ressourcen sehr vieler Hosts, wie sie bei Zählpixeln (und Mash-Ups) üblich sind, sollte vermieden werden.

Reduziere SSL-Handshakes

Es ist zu im Einzelfall zu überprüfen, ob es sich bei einer Seite, die über HTTPS ausgeliefert wird, überhaupt lohnt, verschiedene parallele Hosts zu verwenden. Evtl. wird die Zeit, die durch parallele Downloads gewonnen wird, durch einen zusätzlichen SSL-Handshake wieder verbraucht.

Verwende ein CDN

Content Delivery Networks bieten ein Netzwerk aus Servern, die an verschiedenen Standpunkten gehostet werden. Über dieses Netzwerk lassen sich Ressourcen wie Videos oder große Downloads räumlich verteilen. Nutzern, die eine Ressource aus dem CDN anfordern, wird diese automatisch über einen räumlich nahe liegenden Server ausgeliefert. Große Dateien (wie Trailer und andere Flash-Movies) werden über ein CDN ausgeliefert. Flash-Movies sind grundsätzlich so zu gestalten, dass sie von einer beliebigen URL ausgeliefert werden können. Dies ist wegen der Verwendung von ActionScript und der Same Origin Policy nicht selbstverständlich.

Verwende einen cookie-freien Host für statischen Content

Cookies werden bei jedem Request der Cookie-Domain vom Browser zum Server übertragen. Die Bandbreite vom Browser zum Server ist oft viel kleiner als die Bandbreite vom Server zum Browser. Das Übertragen dieser Cookies ist für viele Inhalte unnützer Overhead. Content, der statisch ist, also keine Session-Informationen o.ä. aus Cookies benötigt, sollte darum von einer zweiten, Cookie-freien Domain ausgeliefert werden. Beispiele für solchen Content sind Stylesheets, Grafiken und Flash-Movies, die nicht über das CDN (siehe oben!) ausgeliefert werden. Ein URI-Builder in einem CMS, der zwischen statischen und dynamischen Inhalten unterscheiden kann, liefert dynamische Inhalte ohne Hostname, statische Inhalte jedoch über einen entsprechend konfigurierten Host.

Optimiere Stylesheets

HTTP-Requests  lassen sich leicht reduzieren, indem man CSS-Dateien zu wenigen oder zu einer einzigen Ressource zusammenfasst. Überflüssige White-Spaces lassen sich in den CSS-Dateien über den YUI-Compressor entfernen. Diese Komprimierung sollte automatisiert im Build bzw. im Publikationsprozess stattfinden. Da CSS-Expressions und lange CSS-Selektoren sich negativ auf die Browser-Performance auswirken, sollten diese vermieden werden. Background-Grafiken in Stylesheets lassen sich zu CSS-Sprites zusammenfassen. Wenn es auf einer Seite 25 grafische Elemente gleicher Größe gibt, lassen sich diese zu einer Sprite-Map mit 5×5 Elementen zusammenfassen. Statt 25 HTTP-Requests findet dann nur noch ein einziger HTTP-Request statt. Ein gutes Beispiel für CSS-Sprites ist die Sprite-Map, die Google auf der Suchseite verwendet. Das Bookmarklet auf spriteme.org kann automatisch Sprite-Maps einer Seite generieren.

Optimiere JavaScript

JavaScripts, die auf verschiedenen Seiten benötigt werden, sollten nicht inline im HTML-Dokument sondern als eigene JavaScript-Ressource verwendet werden. So wird die Größe des einzelnen HTML-Dokuments reduziert. JavaScripte, die nur auf einer Seite verwendet werden, sollten inline im HTML-Dokument stehen, um die Anzahl der Requests zu reduzieren. Viele Browser laden JavaScript – im Gegensatz zu anderen Ressourcen wie Grafiken oder Stylesheets – nicht parallel sondern arbeiten ein JavaScript-Dokument nach dem anderen ab. Der Download von JavaScript blockiert also den parallelen Download von Ressourcen. Darum machen sich Optimierungen in JavaScript-Bibliotheken besonders stark bemerkbar. Ähnlich wie bei CSS-Dateien lassen sich auch JavaScript-Bibliotheken zu wenigen JavaScript-Ressourcen zusammenfassen. Dies hat zudem den Vorteil, dass Abhängigkeiten zwischen Bibliotheken explizit gemacht werden. Wenn eine Bibliothek wie jQuery-UI von der Bibliothek jQuery abhängt, dann muss jQuery in der einzigen Bibliotheks-Ressource vor jQuery-UI eingebunden werden. Das Zusammenfassen von JavaScript-Bibliotheken zu einer einzigen JavaScript-Ressource sollte im Build-Prozess durch einen Assembler erfolgen. Durch das Verwenden eines Assemblers im Build-Prozess lässt sich zudem sicher stellen, dass Scripte nicht doppelt eingebunden werden. Die Größe von JavaScript-Dokumenten lässt sich zudem leicht durch das Minifizieren reduzieren. Durch das Minifizieren werden Kommentare und überflüssige Whitespaces aus dem JavaScript entfernt. Gängige Kompressoren sind Google Closure, JSMin von Douglas Crockford oder YUI-Compress von Yahoo. Auch das Minifizieren von JavaScript sollte automatisiert im Build-Prozess erfolgen. Alternativ lässt sich auch zur Laufzeit JavaScript-Code über den YUI-compressor komprimieren. Dazu lässt sich der Google-HTML-Kompressor (htmlcompressor.googlecode.com) in Verbindung mit dem YUI-Compressor verwenden. JavaScripte, die nicht schon vor dem Rendern der Seite benötigt werden, lassen sich leicht in den Fuß des Dokuments packen. So werden sichtbare Elemente vor den JavaScript-Ressourcen geladen und die Seite wird bereits dargestellt, während JavaScript-Bibliotheken, die im Fuß der Seite eingebunden wurden, noch nachladen. JavaScripte sollten so erstellt werden, dass sie nicht über document.write() direkt in den HTML-Strom schreiben sondern das DOM nach dem Laden der Seite manipulieren. So wird verhindert, dass der Browser das Rendering blockiert oder sogar unnötig oft ein Re-Rendering durchführen muss.

Verwende den Expires-Header

Beim ersten Besuch einer Seite muss der Browser alle Inhalte herunter laden. Diese Inhalte lassen sich im Browser-Cache zwischen speichern. Ob und wie lange diese Inhalte gespeichert werden, wird über HTTP-Header wie den Expires-Header gesteuert: Expires: Wed, 14 Oct 2010 09:01:30 GMT Für statischen Inhalt lässt sich dieser auf beispielsweise ein Jahr in die Zukunft setzen. So wird der Browser veranlasst, Dokumente möglichst lange zwischen zu speichern. Man kann den Header auch weiter in die Zukunft setzen, allerdings wird dies im RFC nicht empfohlen. Der Expires-Header hat den Nachteil, dass er ein Datum erwartet. Eigentlich müsste also die Zeit zwischen Server und Browser synchronisiert sein. Statt dem Expires-Header wurde in http 1.1 darum die Max-Age-Direktive des Cache-Control-Headers eingeführt, die eine Angabe in Sekunden erwartet:
Expires: Wed, 14 Oct 2010 09:01:30 GMT Cache-Control: max-age=31536000

Obwohl der Cache-Control-Header den Expires-Header ablösen sollte, empfiehlt es sich, beide Header übereinstimmend zu setzen. Dies kann das mod_expires-Modul des Apaches zu übernehmen. Wenn nun allerdings statischer Inhalt, der ein Jahr im Cache des Browsers liegen soll, auf dem Server geändert wird, so bekommt der Nutzer dies nicht mehr mit. Darum muss neben dem Setzen von Expires-Headern auch dafür gesorgt werden, dass die veränderten Inhalte eine neue (eindeutige) URL bekommen. Ein URI-Builder integriert darum das Release-Datum einer Ressource in die URL der Ressource:
content/static/5940026/2009-10-12-11-52-39/thumbnail.gif

Wenn eine neue Version der Ressource veröffentlicht wird, dann wird im HTML-Dokument automatisch eine neue URL generiert. Der Browser holt dann die Ressource nicht mehr aus dem Cache sondern holt sich die neue Version der Ressource ab. Statt einer eindeutigen URL ließe sich auch der/das ETAG verwenden. Ein ETAG kann man sich wie eine Prüfsumme über den Content vorstellen. Der Server überträgt die Prüfsumme (den ETAG) zusammen mit dem Content im HTTP-Header. Wenn der Browser die Ressource erneut benötigt, überträgt er beim GET-Request den ETAG der Ressource mit, die er in seinem Cache findet. Der Server kann nun, wenn sich der ETAG nicht geändert hat, mit einer http-304-Response (Not Modified) antworten. Gegenüber einer eindeutigen URL hat der ETAG zwei Nachteile. ETAGs lassen sich ein einem Verbund von Apaches (Loadbalancing) nur schwer eindeutig konfigurieren und ETAGs benötigen für jede angeforderte Ressource einen http-Roundtrip. Darum sind Expires- und Cache-Control-Header dem ETAG vorzuziehen. ETAGs, die der Tomcat setzt, sollten vom Apache entfernt werden!

Komprimiere Ressourcen

Textuelle Ressourcen (HTML, JavaScript und CSS) lassen sich komprimiert übertragen. Bereits komprimierte Ressourcen wie Grafiken und PDFs sollten nicht zusätzlich noch einmal komprimiert werden. Die gebräuchlichste Komprimierung ist die GZIP-Komprimierung. Browser, die eine komprimierte Übertragung unterstützen, teilen dies beim GET-Request dem Server mit: Accept-Encoding: gzip, deflate Wenn der Server dann die Ressource komprimiert ausliefert, antwortet er mit dem Response-Header: Content-Encoding: gzip Am Apache einschalten lässt sich GZIP mit dem Modul mod_gzip (Apache 1.3) bzw. mod_deflate (Apache 2.x). Obwohl der Internet Explorer 6 offiziell Kompression unterstütz, kommt es doch hin und wieder zu Fehlern, z.B. wenn auf dem gleichen Rechner eine alte Version des RealPlayers installiert ist. Um auf Nummer Sicher zu gehen, sollte man darum die Kompression nur für gängige Browser, die neuer als der Internet Explorer 6 sind, aktivieren. Leider interpretieren einige Proxies, die z.B. in Unternehmen eingesetzt werden, den Content-Encoding-Header falsch. Dies kann dazu führen, dass ein Browser, der GZIP unterstützt, vom Proxy nicht-gezipte Ressourcen ausgeliefert bekommt, oder schlimmer, dass ein Browser, der keine Kompression unterstütz, vom Proxy komprimierte Inhalte bekommt. Darum müsste dann der Vary-Header entsprechend so gesetzt werden, dass ein Proxy je nach Encoding und Browser (User-Agents) unterschiedliche Ressourcen für unterschiedliche Encodings und Browser (User-Agents) zwischenspeichert:
Vary: Accept-Encoding,User-Agent
Leider landen so sehr viele Artefakte im Cache, weswegen der Vary-Header nicht wie oben beschrieben konfiguriert wurde. Es wird also hingenommen, dass fehlerhafte Proxies und IE6-Versionen eine nicht optimale Darstellung der Seite erhalten. Vor dem Zippen des HTML-Contents lässt sich dieser über den Code-Kompressor minifizieren. Dadurch lassen sich überflüssige HTML-Kommentare, Whitespaces etc. entfernen:

<%@ taglib uri="http://htmlcompressor.googlecode.com/taglib/compressor" prefix="compress" %>
<compress:html enabled="true" compressJavaScript="true">
<!-- here is your JSP code -->
</compress:hml>

Ermögliche Proxy-Caching

Obwohl es Probleme mit Proxies geben kann (siehe oben!), solle das Caching durch Proxies trotzdem generell motiviert werden. Dies erreicht man, indem man den Cache-Control-Header auf Public setzt. So teilt man nicht nur dem Browser, sondern auch Proxies explizit mit, dass Inhalte gecacht werden dürfen. Cache-Control: Public Einige Browser lassen sich durch diesen Header zusätzlich überreden, Ressourcen, die über HTTPS ausgeliefert werden, zwischen zu speichern, obwohl im Normalfall Ressourcen, die über eine geschützt Verbindung ausgeliefert werden, nicht auf der Festplatte zwischengespeichert werden sollen.

Über den Autor

Oliver Ochs

Oliver baut Websites seit 1998 und verbringt seitdem nahezu jeden Tag damit, das Web besser, schneller und einfacher zu machen. Er gibt dazu Coachings, veranstaltet Workshops und arbeitet in Projekten. Dabei liebt er es, komplexe Performance- und Front-End-Probleme zu lösen. Er ist ein Frontend-Op, der sich als Full-Stack-Developer in Java und JS an der Schnittstelle zwischen UI und Backend am wohlsten fühlt.

2 Kommentare

Antwort hinterlassen