Caching mittels HTTP Header +
Nachdem wir unsere Web-Anwendung komprimiert ausliefern, kommen wir nun zu weiteren Optimierungen. Thema dieses Blog-Eintrags ist unter anderem das Caching über den Expires-Header.
Was passiert normalerweise bei dem Aufruf einer Webseite? Betrachten wir einmal die Webseite http://www.agimatec.de
Zu Beginn wird die HTML-Datei geschickt. Diese wird vom Browser eingelesen und auf externe Ressourcen überprüft, die nachgeladen werden müssen. Der Browser trifft dabei auf zwei CSS-Dateien die er als nächstes anfragt und ebenfalls einliest. In der CSS-Datei sind Referenzen auf Bilder, die nachgeladen werden. Während der Browser Dateien nachlädt fängt er schoneinmal mit dem Rendering an. Das typische Aufbauen einer Webseite ist sichtbar.
Insgesamt werden 14 Requests losgeschickt, die viel Spielraum für Verbesserungen bieten. Einerseits könnte man die Webseite anders aufbauen. Dazu versucht man möglichst viel Layout über CSS zu lösen und Bilder nur dort einzusetzen, wo sie unbedingt notwendig sind. Die Bilder im CSS könnten dann noch durch CSS-Sprites zusammengefasst werden. Manches davon ist sehr zeitaufwendig und führt gerade bei Layout-Überarbeitungen erstmal zu den typischen Browser-Inkompatibilitäten.
Warum nicht (auch) da ansetzen, wo es wesentlich einfacher ist – auf dem Server. Dazu ist es unumgänglich sich mit dem Hypertext-Transfer-Protocol auseinanderzusetzen.
Punkt 1 Komprimierung:
Sendet der Browser den HTTP-Header Accept-Encoding: gzip, deflate so könnte die Übertragung von HTML, CSS und Javascript komprimiert erfolgen. Dies spart ca. 75% der übertragenen Datenmenge. Klingt viel und ist es auch. Natürlich geht etwas CPU-Leistung in das Komprimieren und Entpacken. Dies ist aber in den meisten Fällen zu vernachlässigen. Trifft der Server auf einen Browser, der das entsprechende Accept-Encoding beherrscht, so antwortet er mit dem HTTP-Header: Content-Encoding: gzip
Im Fall des Apache 2 ist nicht viel mehr als das Modul mod-deflate notwendig, um 75% zu sparen. Jetzt mal überlegen was man dafür alles auf der Seite zu optimieren hätte.
Punkt 2 Caching:
Nachdem wir also den initialen Aufruf der Webseite erheblich beschleunigt haben, wäre es doch super weitere Aufrufe der Webseite zu optimieren. Wann ist was wichtig? Kommt ganz auf die Webseite bzw. Web-Anwendung an. Geht man davon aus, dass der Benutzer auf vielen Seiten der Anwendung unterwegs ist, sollte man sehr viel Wert auf ein optimiertes Caching legen. Gibt es dagegen nur viele Treffer auf der Landing-Page, so sollte darauf geachtet werden, diese so klein wie möglich zu gestalten. Unser Geschäft sind Web-Anwendungen auf Basis eines Portals in dem die User mehrere Stunden am Tag unterwegs sind. Somit ist das richtige Caching unglaublich wichtig.
Was passiert normalerweise? Die Ressourcen, die der Browser beim ersten Aufruf heruntergeladen hat, finden sich im Browser-Cache wieder. Mit dabei das Datum, an dem sich die Datei zum letzten Mal geändert hat. Wird dieselbe Seite oder eine andere Seite mit den gleichen Ressourcen aufgerufen, so schickt der Browser Requests mit dem HTTP-Header: If-Modified-Since: Wed, 16 Apr 2008 18:09:44 GMT
Der Server überprüft, ob eine neuere Version der angeforderten Ressource vorhanden ist. Findet er eine neuere Version so schickt er diese mit dem Status Code 200 heraus. Hat sich aber nichts an der Datei geändert, so wird einfach der Status-Code 304 (Not modified) zurück gesendet. Das spart die vollständige Übertragung der Datei. Klingt gut, aber es geht besser. Überlegt man, was sich an einer Webseite ändert, so stellt man schnell fest, dass das Design bestehend aus CSS und Bildern nie geändert wird. Trotzdem werden 12 Requests zum Server gesendet, um festzustellen, dass sich nichts geändert hat. Und hier schlägt der HTTP-Header Expires zu, über den ich den Artikel eigentlich nur verfassen wollte. Alle statischen Ressourcen sollten mit dem Header Expires: Wed, 16 Apr 2015 12:00:00 GMT (also irgendwas in ferner Zukunft) versehen werden. Plötzlich fragt der Browser nur noch die HTML-Seite an. Vielleicht 2 Kilobyte bei einer kleinen Seite. Keine Bilder, kein CSS, kein Javascript. Es ändert sich ja nicht.
Und wenn es sich doch ändert? Dann sollte über neue Namen für die Ressourcen dafür gesorgt werden, dass diese heruntergeladen werden. Also einfach Versionsnummern aufnehmen und schon hat man das perfekte Caching.
Wie setzt man den Expires-Header? Der Apache hat das schöne Modul mod-expires, welches sich nach wenigen Zeilen Konfiguration um alles kümmert. Setzt man aber einen Tomcat dahinter über einen Connector ein, so ist das richtige Handling schwierig. Aus diesem Grund kann ich einen primitiven Servlet-Filter präsentieren, der das Setzen des Headers übernimmt und in der Datei web.xml über das Url-Pattern sehr gut kontrolliert werden kann.
import javax.servlet.*;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.Date;
/**
* This filter is used to optimize the HTTP Headers for caching.
* Just use it for static files or non-changing requests.
*/
public class CacheOptimizationFilter implements Filter {
public void init(FilterConfig filterConfig) throws ServletException {
}
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
HttpServletResponse response = (HttpServletResponse)servletResponse;
response.setDateHeader("Expires",new Date().getTime() + 1000*60*60*24*365*10);
filterChain.doFilter(servletRequest,response);
}
public void destroy() {
}
}
Ich hoffe der Vortrag war lehrreich und konnte zeigen, warum die Beschäftigung mit Protokoll-Headern richtig sinnvoll sein kann.
private static final long tenYearsInMilliseconds = new GregorianCalendar(2010, 0,1,12,00,00).getTimeInMillis() – new GregorianCalendar(2000, 0,1,12,00,00).getTimeInMillis(); should be faster if the VM is not doing compiler optimization for static calculations. Readability will also be better.