Webseiten-Performance erhöhen

Dwain Chambers 100m semisSeit ein paar Wochen versuche ich nach und nach die Performance dieses Blogs zu verbessern. Dabei achte ich insbesondere auf die Leistungsanzeige bei den Google Webmaster Tools, weil diese nicht nur meinen Mittelwert anzeigen, sondern auch den 20%-Quantil aller Webseiten. Sicherlich ist das keine repräsentative Anzeige, aber immerhin eine brauchbare Vergleichsgröße. Momentan pendelt mein dortiger Wert grob zwischen 2 und 5 Sekunden, mein Ziel sind möglichst konstante Werte um 2 Sekunden. Bei der Optimierung greife ich auf die Erweiterungen Google Page Speed und Yahoo! YSlow für Firefox mit Firebug zurück, die mich mit vielen Detailinformationen über meine Seite unterstützen.

In diesem Beitrag möchte ich einige Techniken erläutern, die die Performance (die Ladezeiten) einer Webseite deutlich erhöhen können. Nicht jeder Vorschlag macht in allen denkbaren Zusammenhängen Sinn, weshalb ihr natürlich immer noch abwägen müsst, ob Nutzen und Aufwand in einem angemessenen Verhältnis stehen. Wer sich an dieser Stelle fragt, wieso die Ladezeiten denn so wichtig seien, dem möchte ich nochmals meinen Artikel über das Kurzeitgedächtnis und Webdesign empfehlen. Doch jetzt direkt in die Praxis:

  1. HTTP-Requests minimieren
  2. Reihenfolge beachten
  3. Dateigrößen minimieren
  4. Komprimierung aktivieren
  5. Cache einsetzen
  6. DNS-Abfragen einsparen
  7. Cookies überdenken
  8. Schlussbemerkung

HTTP-Requests minimieren

Beispiel: HTTP-Request QueueIm Zeitalter der Breitbandzugänge sind weniger die tatsächlichen Dateigrößen, sondern vielmehr die Zahl der einzelnen Zugriffe der Flaschenhals einer Internetverbindung. Beispielsweise ist die Zahl der gleichzeitigen Verbindungen bereits browserseitig auf meist weniger als 5 eingeschränkt. Vergleichbare Einschränkungen gibt es natürlich auch durch die Webserver.

Wird nun eine Webseite aufgerufen, so werden die im HTML-Dokument eingebauten Elemente wie CSS-Dateien, Bilder und JavaScript-Dateien mit jeweils einer eigenen Abfrage geladen. Obwohl die Bandbreite theoretisch den quasi zeitgleichen Download aller Einzelelemente erlauben würde, werden die Dateien mit erreichen der Request-Beschränkung in eine Warteschlange gepackt.

Beispiel: Eine Webseite enthält 10 Grafiken, aber es sind nur 4 gleichzeitige Verbindungen erlaubt. Also werden nur 4 der 10 Grafiken geladen und die anderen 6 in die Warteschlange (Queue) gesetzt. Erst wenn eine der ersten 4 Grafiken fertig geladen wurde, kann die erste Grafik aus der Warteschlange nachrücken (vgl. obige Abb.).

Hier nun zwei Möglichkeiten, um die Zahl der einzelnen Abfragen einzugrenzen:

  1. Unnötige Dateien entfernen: Im Laufe der Zeit können sich viele Dateien ansammeln, die mittlerweile nicht mehr notwendig sind. Wird beispielsweise wirklich jeder eingebundene Button tatsächlich benötigt? Werden die einzelnen Module des JavaScript-Frameworks wirklich alle verwendet? Ist es notwendig solche Module auf jeder Einzelseite zu laden? Wurden vielleicht irgendwelche Plugins oder sonstige Erweiterungen nicht korrekt entfernt, so dass nicht mehr verwendete Skripte geladen werden? Es empfiehlt sich den eigenen Quellcode nach solchen Karteileichen abzusuchen und von ihnen zu befreien.
  2. Einzeldateien zusammenfassen: Während der Entwicklung oder des Designs mag es durchaus sinnvoll erscheinen, Stylesheets oder Skripte thematisch in einzelne Dateien aufzuteilen. Für die endgültige Darstellung der Webseite ist es aber nicht relevant, ob es nun eine navigation.css, eine header.css, etc. gibt, oder ob all diese Stylesheets nun in einer style.css zusammengefasst werden. Übrigens können auch Grafiken in einer Datei zusammengefasst werden, wie ich bereits im Artikel über CSS-Sprites erläutert habe. Zur Entwicklung kann man seine Dateien natürlich getrennt halten, aber die Release-Version sollte dann entsprechend optimiert werden. [Update] Wie man JavaScript-Dateien mit Google bündelt wurde ebenfalls heute nebenan bei Sergej Müller erläutert.[/Update]

Außerdem wird durch eine Vielzahl von HTTP-Requests auch die Größe der einzelnen Dateien unnötig aufgebläht. Denn immerhin werden für jede Datei Header ausgetauscht und ggf. auch Cookies mitgesendet, so dass die Metadaten schnell mehr Speicherplatz benötigen als die Datei selbst.

Reihenfolge beachten

Eine häufig vertretene Meinung ist es, dass die Reihenfolge der Angaben im Head-Bereich des HTML-Dokumentes vollkommen egal sei. Dies ist aber nur bedingt richtig, wie z.B. dieser „Best Practices“-Artikel von Page Speed zeigt.

Eine JavaScript kann die Struktur einer Webseite verändern. Also erscheint es durchaus als sinnvoll, zunächst ein JavaScript auszuführen, bevor das nächste CSS-File interpretiert wird. Immerhin müssten sonst das Rendering wiederholt werden. Viele Browser laden und interpretieren deshalb zunächst die bereits anstehenden Skript-Dateien, bevor sie mit der nächsten CSS-Datei fortfahren. Folglich ist es durchaus sinnvoll, erst die gesamten Stylesheets zu laden und dann die Skripte… es sei denn, diese sollen ganz bewusst erst das Layout umbauen und dann soll die Seite gerendert werden, auch wenn mir der Sinn einer solchen Vorgehensweise nicht wirklich einleuchtet 😉

Beispiel:
[cce lang=“html“]
<link rel=“stylesheet“ type=“text/css“ href=“style1.css“ />
<script type=“text/javascript“ src=“myscript.js“ />
<link rel=“stylesheet“ type=“text/css“ href=“style2.css“ />
[/cce]

So würden viele Browser zunächst javascript1.js laden und ausführen, bevor style2.css geladen wird. Besser wäre also:

[cce lang=“html“]
<link rel=“stylesheet“ type=“text/css“ href=“style1.css“ />
<link rel=“stylesheet“ type=“text/css“ href=“style2.css“ />
<script type=“text/javascript“ src=“myscript.js“ />
[/cce]

Aus dem ersten Abschnitt sollte aber auch klar sein, dass folgende Fassung noch besser wäre:

[cce lang=“html“]
<link rel=“stylesheet“ type=“text/css“ href=“style.css“ />
<script type=“text/javascript“ src=“myscript.js“ />
[/cce]

style1.css und style2.css sind also in einer Datei style.css zusammengefasst.

Dateigrößen minimieren

Obwohl, wie eingangs erwähnt wurde, die Bandbreite heutzutage ein eher geringeres Problem sein sollte, darf man nicht verschwenderisch mit ihr umgehen. Immerhin kann die wirklich verfügbare Bandbreite oft von der theoretisch möglichen Abweichen, sei es auf Nutzerseite, auf Serverseite oder irgendwo auf dem Weg dazwischen. Durch das immer beliebtere Surfen via Mobile muss man zudem auch mit dem ein oder anderen Besucher via Edge statt UMTS rechnen, genauso wie immer noch nicht jeder zu Hause über einen Breitbandanschluss verfügt. Aber eine Reihe kleiner Kniffe kann die Größe einzelner Dateien bereits drastisch senken und so zu einem besseren Surferlebnis für alle Besucher führen:

Wiederholungen vermeiden

Sowohl innerhalb von Skripten, als auch von CSS-Dateien schleichen sich gerne unnötige Wiederholungen ein, die problemlos vermieden werden können, sofern man sein Augenmerk einmal darauf richtet.

Beispiel JavaScript: Denken wir uns, dass es im Code eine Vielzahl von JavaScript-Funktionen gäbe, die alle einen bestimmten Ergebniswert mit nur zwei Nachkommastellen in eine Textbox schreiben und zudem die Textbox auf „readonly“ setzen sollen. So könnten die letzten Zeilen all dieser Funktionen aussehen:

[cce lang=“javascript“]
//… Berechnungen …
output = result.toFixed(2);
document.getElementById(‚resultbox‘).readOnly = true;
document.getElementById(‚resultbox‘).value = output;
[/cce]

Da diese Befehlsfolge in allen Funktionen auftaucht, evtl. nur mit einer wechselnden Textbox für die Ausgabe, können wir sie in eine eigene Funktion schreiben:

[cce lang=“javascript“]
function show_result(id, value) {
output = value.toFixed(2);
document.getElementById(id).readOnly = true;
document.getElementById(id).value = output;
}
[/cce]

Die Funktionen nach dem ersten Beispielcode müssen jetzt also nur noch show_result(‚resultbox‘, result); aufrufen. Unsere neue Funktion kann man natürlich auch noch kürzen:

[cce lang=“javascript“]
function show_result(id, value) {
e = document.getElementById(id);
e.readOnly = true;
e.value = value.toFixed(2);
}
[/cce]

Vielleicht wird sogar schon in den aufrufenden Funktionen mit dem Objekt aus getElementById gearbeitet? Dann übergeben wir natürlich nicht die id sondern gleich das Objekt und sparen uns das getElementById vollständig.

Beispiel CSS: Schauen wir uns mal folgende CSS-Klassen für eine Navigation an:

[cce lang=“css“]
.nav1 { background:#cc0000; font-size: 1em; font-weight: bold; color: #fff; text-decoration: none; }
.nav2 { background:#cccc00; font-size: 1em; font-weight: bold; color: #fff; text-decoration: none; }
.nav3 { background:#00cc00; font-size: 1em; font-weight: bold; color: #fff; text-decoration: none; }
.nav4 { background:#00cccc; font-size: 1em; font-weight: bold; color: #fff; text-decoration: none; }
.nav5 { background:#0000cc; font-size: 1em; font-weight: bold; color: #fff; text-decoration: none; }
[/cce]

Der zugehörige HTML-Teil:

[cce lang=“html“]
<div class=“nav1″>Text 1</div>
<div class=“nav2″>Text 2</div>
… usw. …
<div class=“nav5″>Text 5</div>
[/cce]

Die gesamte Textformatierung wiederholt sich für alle Klassen. Wieso diese also nicht auslagern? Beispielsweise so:

[cce lang=“css“]
.nav1 { background:#cc0000; }
.nav2 { background:#cccc00; }
.nav3 { background:#00cc00; }
.nav4 { background:#00cccc; }
.nav5 { background:#0000cc; }
.navtext { font-size: 1em; font-weight: bold; color: #fff; text-decoration: none; }
[/cce]

Und entsprechend im HTML-Abschnitt:

[cce lang=“html“]
<div class=“navtext nav1″>Text 1</div>
<div class=“navtext nav2″>Text 2</div>
… usw. …
<div class=“navtext nav5″>Text 5</div>
[/cce]

Alternativ kann man natürlich auch Elemente verschachteln und dem Elternelement die immer wiederkehrenden Texteigenschaften zuordnen und den Unterelementen nur den zugehörigen Hintergrund. Wichtig ist natürlich, dass solche Änderungen nur durchgeführt werden, wenn sie auch wirklich Sinn machen. Sagen wir im obigen Stylesheet gäbe es auch die folgende Zeile:

[cce lang=“css“]
a { background:#000000; font-size: 1em; font-weight: bold; color: #fff; text-decoration: none; }
[/cce]

Hier sollte die eigene Notation der Texteigenschaften bestehen bleiben, da wir sonst zu jedem Link der Seite die Klasse navtext hinzufügen müssten, was im Zweifel mehr Output verursacht als diese einmalige Definition. Außerdem dürfte es durchaus sinnvoll sein die Linkformatierung unabhängig von der Navigationsformatierung ändern zu können 😉

Unnötige Steuerzeichen vermeiden

Ebenfalls in JavaScript- und CCS-Dateien (und auch im HTML-Quelltext selbst) begegnen uns viele unnötige Steuerzeichen. Teilweise sind diese versehentlich in die Datei gerutscht, teilweise wurden sie aber auch absichtlich zur besseren Lesbarkeit in den Code eingefügt. Als Entwickler mag man es natürlich, wenn solche Dateien ordentlich strukturiert werden, aber jeder Tab, jedes Leerzeichen und jeder Zeilenumbruch verursacht auch wieder ein Datenbyte.

Darauf muss (und sollte!) man natürlich nicht verzichten, aber wie beim Zusammenfassen von Dateien kann man auch hier eine schön strukturierte Variante für die Entwicklung verwenden, die dann zur Veröffentlichung entsprechend zusammengeschrumpft wird. Hier helfen auch verschiedene Tools, z.B. bietet die anfangs erwähnte Google Page Speed-Erweiterung genau diese Funktionalität an.

Ausschnitt: Nicht optimierte CSS-DateiBeispiel: In der nebenstehenden Grafik sehen wir eine nicht optimierte CSS-Datei. Sie enthält einmal verschiedene Steuerzeichen, die einer besseren Lesbarkeit dienen sollen, aber auch ein paar unnötige Steuerzeichen (die Leerzeichen in der color-Zeile und hinter dem schließenden }).

In diesem Beispiel können wir tatsächlich alle diese Steuerzeichen, abgesehen von einem Zeilenumbruch am Zeilenende, entfernen:

[cce lang=“css“]
.nav1{background:#cc0000;font-size:1em;font-weight:bold;color:#fff;text-decoration:none;}
[/cce]

Damit hätten wir 25 Zeichen abzgl. des abschließenden Umbruchs (also genau 24 Byte, bei Windows-Zeilenumbrüchen sogar mehr) in einer einzelnen Klasse eingespart! Nutzt man dazu ein entsprechendes Tool lassen sich bei komplexen Dateien locker mehrere Kilobyte einsparen. Wichtig ist natürlich nur, dass man keine notwendigen Leerzeichen streicht 😀

Übrigens werden deshalb viele komplexe JavaScript-Frameworks in zwei Varianten ausgeliefert: Eine hübsch formatierte Version für die Entwicklung und eine auf diese Weise komprimierte Version für den Web-Einsatz.

Grafiken optimieren

Oft wird auch die Optimierung der Grafiken vernachlässigt. Der erste Schritt hierbei ist schon die Auswahl des richtigen Formats. Beispielsweise hat das Teaser-Foto dieses Beitrags als JPEG (85%) knapp 11,8 Kilobyte, während es als GIF 21,3 Kilobyte benötigen würde. Die Grafik zu den HTTP-Requests benötigt hingegen als GIF 8,5 Kilobyte, während die gleiche Grafik als JPEG (85%) nicht nur qualitativ schlechter ist, sondern direkt 28,8 Kilobyte verschlingt.

Auch die Kompressionsrate ist natürlich wichtig. Das Teaser-Foto benötigt als JPEG (85%) wie gesagt nur 11,8 Kilobyte. 31,2 Kilobyte würden fällig, wenn das Foto JPEG (100%) gespeichert wäre. Der leichte Qualitätsverlust ist bei diesem in der Darstellung kleinen Bildchen in Relation zu der Einsparung durchaus vertretbar.

Nicht optimierte Grafik: 22,9 KilobyteOptimierte Grafik: 18,3 KilobyteOft hängt die finale Dateigröße von vielen Parametern ab. Hier sehen wir zwei PNG-Grafiken, die offensichtlich absolut identisch sind. Aber während die linke Version 22,9 Kilobyte groß ist, benötigt die rechte Fassung lediglich 18,3 Kilobyte (woran man bei dem kleinen Bild natürlich sieht, dass das Motiv nicht wirklich für PNG geeignet ist). Wo ist der Unterschied? Variante 1 wurde einfach mal schnell via GIMP gespeichert, frei nach dem Motto „ist ja Kompressionsgrad 9“. Für Variante 2 habe ich nochmal OptiPNG angeworfen und immerhin 4,6 Kilobyte einsparen können. Die Arbeit wäre natürlich nicht nötig gewesen, wenn ich bereits beim Speichern die richtigen Parameter gesetzt hätte.

Aber oft speichert man die Grafiken ja nicht zwingend selbst, z.B. bekommt man ein Banner oder einen Button zur Verfügung gestellt. Hier ist ein Blick auf die Optimierung der Grafik nie verkehrt. Übrigens hilft auch hier das Optimierungs-Feature der Google Page Speed-Erweiterung.

Komprimierung aktivieren

Wenn nun alle Textfiles keine unnötigen Wiederholungen und Steuerzeichen mehr enthalten und auch alle Grafiken optimiert sind, können immer noch einige Byte oder gar Kilobyte Daten eingespart werden. Dazu muss man den Server anweisen die Daten vor der Auslieferung zu komprimieren, wobei hier eine richtige gzip-Komprimierung statt der bisherigen „Zeicheneinsparung“ gemeint ist.

Beispiel: Ich habe jetzt einfach mal den gesamten bisher für diesen Artikel geschrieben Text samt HTML in eine Textdatei gespeichert. Diese ist nun stolze 18,7 Kilobyte groß. Nachdem ich sie nun mit gzip komprimiert habe, belegt sie nur noch 6,9 Kilobyte… also eine Einsparung von 11,8 Kilobyte bzw. knapp 60%. Komprimieren wir so das gesamte HTML-Dokument und die zugehörigen Skript- bzw. CSS-Datein, kann also nochmal eine ganze Menge gespart werden.

Aktivieren kann man die gzip-Kompression z.B. via .htaccess, indem man folgende Zeilen hinzufügt:

[cce lang=“bash“]
<FilesMatch „\\.(js|css|html|htm|php|xml)$“>
SetOutputFilter DEFLATE
</FilesMatch>
[/cce]

Das ganze funktioniert jedoch nur, wenn das Apache-Modul mod_deflate aktiviert ist. Wer sich nicht sicher ist, sollte eine entsprechende Abfrage ergänzen und kann dann die Funktion ohne Ausfall der Seite testen:

[cce lang=“bash“]
<IfModule mod_deflate.c>
<FilesMatch „\\.(js|css|html|htm|php|xml)$“>
SetOutputFilter DEFLATE
</FilesMatch>
</IfModule>
[/cce]

Die FilesMatch-Abfrage enthält einen regulären Ausdruck, der die Kompression (SetOutputFilter DEFLATE) nur aktiviert, wenn eine Plain-Text-Datei (js, css, htm(l), php, xml) angefragt wurde. Natürlich kann das noch ergänzt werden (z.B. txt, php5, …), aber Binärdateien, insbesondere Bilder, sollten dort nicht aufgeführt werden. Denn diese wurden ja hoffentlich schon bestmöglich optimiert, so dass durch eine weitere gzip-Kompression kein nennenswerter Vorteil entstehen würde.

Cache einsetzen

Manche Dateien sollten sich nicht so häufig ändern, z.B. ein statisches Bild. Auch Skripte und Stylesheets werden wohl eher seltener aktualisiert. Deswegen kann man dem Browser und beispielsweise auch einem Proxy die Information geben, dass die entsprechenden Dateien über einen längeren Zeitraum gecached werden dürfen. Auch das geht beispielsweise über die .htaccess:

[cce lang=“bash“]
<FilesMatch „\\.(ico|pdf|jpg|jpeg|png|gif)$“>
Header set Cache-Control „max-age=2678400, public“
</FilesMatch>
<FilesMatch „\\.(css|js)$“>
Header set Cache-Control „max-age=86400, proxy-revalidate“
</FilesMatch>
[/cce]

In diesem Fall wird das Apache-Modul mod_expires benötigt. Mit einer entsprechenden Abfrage, ob das Modul aktiviert ist, sieht es also so aus:

[cce lang=“bash“]
<IfModule mod_expires.c>
<FilesMatch „\\.(ico|pdf|jpg|jpeg|png|gif)$“>
Header set Cache-Control „max-age=2678400, public“
</FilesMatch>
<FilesMatch „\\.(css|js)$“>
Header set Cache-Control „max-age=86400, proxy-revalidate“
</FilesMatch>
</IfModule>
[/cce]

Auch hier unterscheidet FilesMatch wieder einzelne Dateitypen. Grafiken und PDFs sollen über Cache-Control 2678400 Sekunden (31 Tage) und Skripte bzw. Stylesheets 86400 Sekunden (24 Stunden) gecached werden. public gibt bei den Binärdateien an, dass quasi jeder in Frage kommende Cache die Datei vorhalten darf, während proxy-revalidate bei den Plain-Text-Files vorgibt, dass ein Proxy-Server sich abzusichern hat, dass sich die Dateien nicht verändert haben.

Natürlich können hier auch andere Konfigurationen eingesetzt werden. Außerdem kann z.B. via PHP ein spezieller Header für einzelne Dateien gesendet werden. Eine ausführliche Beschreibung der Möglichkeiten gibt es in der zugehörigen Spezifikation RFC 2616, 14.

DNS-Abfragen einsparen

Nun widmen wir uns den DNS-Abfragen. Werden auf einer Webseite externe Ressourcen, z.B. Buttons oder Analyse-Skripte, eingebunden, dann muss deren DNS jeweils aufgelöst werden. Das führt zu weiteren Verzögerungen für jede „fremde“ Domain, die in irgendeiner Form zum Einsatz kommt. Hier muss man wieder überlegen, ob man einzelne Dateien (eben z.B. einen Button) nicht kopieren kann und darf. Bei einem Analyse-Button macht das natürlich recht wenig Sinn, aber ein einfacher Button zum Blog des Kumpels muss nicht zwingend von seinem Webspace geladen werden.

Grundsätzlich sollte man aber beachten, ob es auch erlaubt ist die Grafik auf den eigenen Speicher zu kopieren. Partnerprogramme sehen sowas z.B. nicht immer gerne, da ihnen dadurch Statistiken verloren gehen und sie den Button nicht mehr selbst austauschen können.

Cookies überdenken

Abschließend sollte man dann noch die gesetzten Cookies hinterfragen. Immerhin werden die bei den einzelnen Abfragen immer fleißig hin und her gesendet und können so ein enormes Datenaufkommen verursachen. Deswegen sollte man seine Cookies betrachten und sich folgende Fragen stellen:

  1. Sind alle Cookies bzw. sind alle darin gespeicherten Daten wirklich notwendig? Werden sie überhaupt verwendet?
  2. Ist z.B. ein Analysedienst eingebunden, der gar nicht wirklich beachtet wird? Diese setzen meist recht große Cookies… auch Piwik schaufelt da einige Daten.
  3. Müssen die Daten wirklich für einen längeren Zeitraum beim Benutzer in einem Cookie gespeichert werden? Informationen, die eh nur für den aktuellen Besuch relevant sind, können viel besser in einer serverseitigen Session gespeichert werden.

Schlussbemerkung

Kombiniert man die zuvor erläuterten Methoden geschickt, kann man einige Sekunden beim Aufruf einer Webseite einsparen. Wichtig ist, dass man immer abwägt, ob die Änderung einen wirklichen Effekt hat und die Situation nicht „verschlimmbessert“. Viele Faktoren hat man auch nicht selbst in der Hand, aber es kann mit Blick auf die Besucher nie schaden, die Ladezeit der Webseite zu optimieren.

Ich selbst habe bei rund 10 Sekunden durchschnittlicher Aufrufzeit angefangen und bin nun, wie bereits im ersten Absatz gesagt, gerade in einem Bereich von 2-5 Sekunden angekommen. Mal sehen, wie weit ich dies noch verbessern, und ob ich mein gestecktes Ziel erreichen kann.

Wenn ihr noch andere Optimierungstipps kennt, nur her damit! Ich freue mich über jeden Kommentar.

Teaser-Bild: Dwain Chambers 100m semis – British Champs & Olympic Trials 2008 by Paul Foot [CC-BY-SA] Quelle: Wikipedia

Cookie Consent mit Real Cookie Banner