Was folgt ist eine Mischung aus der sehr guten (!) Artikelserie „Make the web faster“ von google und einigen persönlichen Ergänzungen und Erfahrungen. Erhebt keinerlei Anspruch auf Vollständigkeit und mischt server- sowie clientseitige Tipps querbeet. Ich gehe hier nicht besonders ins Detail, nenne aber genug Stichwörter, zu denen man sich dann schlaugooglen kann. Als Abschluss der Einleitung möchte ich noch auf den Gott der Webperformance verweisen, ohne dessen Bücher und Analysen wir sicher noch nicht da wären wo wir heute sind.
- Javascript so weit wie es geht asynchron laden bzw. ans Seitenende befördern, da Browser JS-Dateien in der Regel nicht parallel laden. Langsam geht es allerdings in die richtige Richtung mit der neuen Browsergeneration (siehe hier für mehr Details, Stichwörter async und defer). Es folgt ein von ncz entliehenes Script, um Javascript asynchron nachzuladen – Details siehe hier.
function loadScript(url, callback){ var script = document.createElement("script") script.type = "text/javascript"; if (script.readyState){ //IE script.onreadystatechange = function(){ if (script.readyState == "loaded" || script.readyState == "complete"){ script.onreadystatechange = null; callback(); } }; } else { //Others script.onload = function(){ callback(); }; } script.src = url; document.getElementsByTagName("head")[0].appendChild(script); }
- Da die Anzahl der maximal möglichen Requests zu einem Server beschränkt sind, lohnt sich oft das verteilen von statischem Content auf Subdomains (=anderer Hostname, also mehr Downloads parallel). Allerdings sollte mans nicht übertreiben, da beim Verteilen auf zuviele verschiedene Server die DNS-Lookup-Zeit mehr ins Gewicht fällt als mehr parallele Downloads.
- In der Rangfolge der externen Dateien erst CSS und dann Javascript, da auf die Art und Weise mit den ersten Javascript-Files bereits begonnen werden kann, während CSS noch downloaded. Wer nach Waterfall Charts googlet, wird sehen was ich meine.
- Die Dimensionen von Bildern direkt in den img-Tag einbauen, da der Browser auf die Art und Weise beim Aufbau der Seitenstruktur direkt schon den Platz „einplanen“ kann, bevor das Bild wirklich geladen wurde
- Cache-Header vernünftig setzen (.htaccess). Könnte etwa so aussehen – ist aber natürlich an die individuellen Bedürfnisse anzupassen:
<IfModule mod_expires.c> ExpiresActive On ExpiresDefault "access plus 3 days" ExpiresByType image/jpg "access plus 2 years" ExpiresByType image/jpeg "access plus 2 years" ExpiresByType image/gif "access plus 2 years" ExpiresByType application/javascript "access plus 2 years" ExpiresByType text/css "modification plus 1 day" </IfModule>
Dabei ist wenn immer möglich der expires-header zu verwenden, da auf die Art und Weise ein http-request gespart wird und auch wenn die Antwort 304 ist, kostet das weitere Zeit.
- HTTP-Requests vermeiden. Imagesprites, Javascript und CSS in der Entwicklung getrennt, im Live-Betrieb minified und zu einer Datei zusammengepackt. Wenn auch schon 4 Jahre alt, hier immernoch mein Lieblingsscript dazu. Die effektive URL für seine Scripts lautet dann etwa
http://www.creatype.nl/javascript/prototype.js,builder.js,effects.js,dragdrop.js,slider.js
Diese werden zusammengecruncht und als eine Datei physisch auf dem Server abgelegt.
- CSS Selektoren nicht unnötig spezifizieren. Statt
body #wrapper #left div#menu
würde es auch
#menu
tun, was dem Browser viel Arbeit beim CSS-parsen erspart. CSS Selektoren werden von rechts nach links abgearbeitet. Ich ertappe mich allerdings selbst oft dabei, einen Selektor zu überspezifizieren, weil ich dadurch besser erkenne, in welchem Kontext er steht. Da dann lieber Kommentare verwenden. Außerdem sinkt der „reuse“-Faktor von überspezifizierten Selektoren, was ja eigentlich den Kern von CSS ausmachen sollte. Siehe dazu Object Oriented CSS
- Javascript-reflows vermeiden. Ein Reflow wird durch so ziemlich alles ausgelöst, was im DOM manipuliert wird. So ist es bspw. empfehlenswert, erst das HTML komplett als String zusammenzubauen, bevor man es ins DOM einfügt da auf die Art und Weise die Anzahl der Reflows minimiert wird. Klassisches Beispiel:
var categories = ["Home","About me","Stuff","Other Stuff"]; $.each(categories,function(elem) { $("ul").append("<li>" + elem + "</li>"); });
ist vergleichen mit
var categories = ["Home","About me","Stuff","Other Stuff"], htmlstr = ""; $.each(categories,function(elem) { htmlstr += "<li>" + elem + "</li>" }); $("ul").html(htmlstr);
um eine ganze Ecke unperformanter.
- Cookies am besten komplett vermeiden, da diese bei jeder neuen Seite erneut im Header übertragen werden. Wenn irgendwie machbar, dann per Session-ID. Wenn unbedingt nötig, dann zumindest nur eine ID und einen hash im Cookie speichern und den dann serverseitig validieren und die Daten so auslesen. Ist ohnehin wesentlich sicherer. Auf jeden Fall liegt die maximal vertretbare Größe bei 1500 byte, was der durchschnittlichen MTU entspricht (Bytes pro übertragenem Paket). Die Cookies immer so spezifisch wie möglich setzen. Sollte der Cookie also nur auf einer Subdomain oder einer einzelnen Seite gebraucht werden, ist er auch nur für diese zu setzen.
- Komprimieren! Lieder deflate statt gzip, einfach ausprobieren.
<IfModule mod_deflate.c> AddOutputFilterByType DEFLATE text/html text/plain text/xml text/css application/x-javascript text/javascript application/javascript application/json </IfModule>
- SQL-Statements in Loops vermeiden. Gerade bei Insert gern genommen:
foreach ($cars as $c) { $query = 'INSERT INTO cars (cartype,horsepower) VALUES("'.$c['type'].'","'.$c['hp'].'")'; mysql_query($query); }
Wesentlich effektiver hingegen:
$sql = array(); foreach ($cars as $c) { $sql[] = '("'.$c['type'].'","'.$c['hp'].'")'; } $query = 'INSERT INTO cars (cartype,horsepower) VALUES '.implode(',', $sql); mysql_query($query);
Auch Insert delayed bietet sich oft an.
- Google Page Speed und YSlow! benutzen um die wirklichen bottlenecks zu finden (premature optimization is the root of all evil wie wir alle wissen).
- Zum rumspielen mit den HTTP-Headern empfiehlt sich folgendes Script:
<?php $lastModified = filemtime(__FILE__); $etagFile = md5_file(__FILE__); $ifModifiedSince=(isset($_SERVER['HTTP_IF_MODIFIED_SINCE']) ? $_SERVER['HTTP_IF_MODIFIED_SINCE'] : false); $etagHeader=trim($_SERVER['HTTP_IF_NONE_MATCH']); header("Last-Modified: ".gmdate("D, d M Y H:i:s", $lastModified)." GMT"); header("Etag: $etagFile"); header('Cache-Control: public'); if (@strtotime($_SERVER['HTTP_IF_MODIFIED_SINCE']) == $lastModified || $etagHeader == $etagFile) { header("HTTP/1.1 304 Not Modified"); } echo "Last modified: ".$lastModified." (".gmdate("D, d M Y H:i:s",$lastModified. "GMT").")<br />"; echo "if-modified-since: ".strtotime($ifModifiedSince)." (".$ifModifiedSince.")<br />"; echo "etag-File: ".$etagFile."<br />"; echo "etag-Header: ".$etagHeader."<br />";
Zum Thema SQL-Statements in einer Schleife… hier seien Prepared Statements ebenfalls bei INSERTs empfohlen, da das Statement DB-seitig nur einmal analysiert, aber x-fach schnell hintereinander mit Daten befüllt ausgeführt wird.