Prerendering und Prefetching

14. Oktober 2011

Beim Prerendering und Prefecthing geht es um zwei unterschiedliche, aber ähnliche Techniken zur schnelleren Darstellung von Webseiten im Browser, nachdem der User auf einen Link geklickt hat.

Es gibt Situationen in denen eine Webseite mit hoher Wahrscheinlichkeit weiß, welche Webseite ein Benutzer als nächstes besucht, z.B. das erste Ergebnis bei der Google Suche oder eine Folgeseite bei einem mehrseitigen Artikel. In genau diesen Fällen erscheint es sinnvoll mit dem Laden dieser Seite zu beginnen während der Benutzer noch die aktuelle Seite betrachtet, so dass beim Click für diese Seite schon möglichst viel Vorarbeit geleistet ist.

Es gibt einige Bedenken und Vorurteile (z.B. dass man damit vermeintlich ungewollt illegale Links aufrufen kann), die gegen den Einsatz dieser Techniken sprechen, die bei genauerer Betrachtung aber recht unbegründet sind; die Entwickler der Browser gehen hier mit Bedacht vor. Außerdem kann man dies in den jeweiligen Einstellungen auch deaktivieren.

Die Requests zu diesen beiden Techniken sind in den Standardentwicklerwerkzeugen der Browser leider nicht sichtbar, so dass man zur Analyse und zum Debugging auf externe Tools zurückgreifen muss, z.B. Wireshark.

HTTP Request Header

Sollten beim Testen unerwartete Ergebnisse zu beobachten sein sollte man bedenken, dass viele Webserver “Prefetch” Anfragen unterdrücken, weil deren Betreiber den Traffic fürchten, der nicht direkt zu einer Page Impression führt.

Man kann dies im HTTP Request Header wie folgt feststellen:

X-moz: prefetch Diesen nicht standardisierten Header schickt Firefox mit, sobald es eine Prefetch Anweisung ausführt.
X-Purpose: instant Google Chrome lädt z.T. eine Webseite, noch bevor man in der URL-Eingabezeile auf Enter drückt, und kennzeichnet dies durch diesen Header
Page Visibility W3C Working Draft Beim Google Chrome Prefetching wird derzeit kein Extra-Header mitgeschickt, man kann dies aber durch die Chrome eigene Page Visibility API in Javascript herausfinden und dort dann im Code entsprechend reagieren. Unter den Entwicklern wird diskutuiert ob in Zukunft ein entsprechender X-Purpose-Header mitgeschickt werden soll.

Der Server kann Prefechtiung auch direkt untersagen, in dem er folgenden Header (oder analog im entsprechenden http-equiv-element im head) mitschickt:

x-dns-prefetch-control: off

Prefetching in Firefox 7

Unterstützte Link-Typen

  • dns-prefetch
  • prefech
  • next

Firefox lädt nur Seiten die im HTTP-Header mittles Link-Element vom Typ “prefetch” oder aber mittels Link-Element vom Typ “next” (allgemeine Informationen zum Link-Element) im aktuellen Dokument angegeben werden (mehr dazu findet man im FAQ von Mozilla). Alternativ ist auch die gleichwertige Schreibweise mittles http-equiv im head-Teil des HTML-Dokumentes zulässig. Dabei muss es eine URL ohne Aufrufparameter sein, die mit dem Protokoll HTTP ausgeliefert wird, da sonst kein Caching erfolgen würde und der Request keinen Mehrwert bringen würde.

Es wird nur das angegebene Objekt (Seite oder Bild) geladen; keine weiteren abhängigen script-, link-, oder img-Elemente einer bereits im Voraus geladenen Seite. Allerdings macht Firefox für die beteiligten Hosts schon einmal alle nötigen DNS-Lookups. Beim Prefetching gibt es keine same-origin Policy, da dies die Browsersicherheit nicht erhöhen würde. Jedes externe Element kann vorgeladen werden, genau wie es direkt in der Webseite geladen werden könnte.

Die Geschwindigkeitsvorteile beim Prefetching sind leider nicht wirklich groß, da der meiste (größte) Content oft in Sub-Ressourcen (CSS, JS, Medien) steckt, die eben nicht vorgeladen werden. Auch per Javascript nachgeladene Fragmente oder Modifikationen werden nicht berücksichtigt.

Prerendering in Chrome 14

Unterstützte Link-Typen

  • dns-prefetch
  • prerender

Prerendering erweitert das Konzept des Prefetchings, ersetzt es seit Chrome 13 sogar komplett. Statt nur das Hauptdokument zu laden werden auch alle Abhängigkeiten geladen und auch alle Elemente fertig geparst. Das Anzeigen einer vorgerenderten Seite kann man dann am ehesten mit einem Tab-Wechsel vergleichen.

Nur noch DNS-Prefetching Anweisungen führen nur dazu, dass die DNS Einträge der entsprechenden Links vorab aufgelöst werden.

Deshalb gibt es auch einen neuen Typ für link-Elemente – “prerender”. Dieser sorgt dafür, dass der Browser nach dem erfolgreichen Laden der aktuellen Seite damit beginnt die dort angegebene Seite zu redern. Zum Testen stellt Google eine Seite bereit, mit der man einfach jede Seite vorrendern lassen und dann einfach “umschalten” kann. Die Ergebnisse sind z.T. wirklich beeindruckend. Hier wird auch deutlich: diese Link-Elemente können auch während der Benutzer bereits der Seite ist mittels Javascript angelegt und verändert werden.

Eine Erkennung wie der Aufruf erfolgt ist (also ob die Seite wirklich angezeigt wird) ist nur im Javascript Kontext möglich, mit der Page Visibility API. Google Analytics z.B. nutzt diese bereits um zu unterscheiden wie die Seite geladen wurde um die Views korrekt zu ermitteln.

Google Chrome ignoriert die link-Typen prefetch und next. Es wird pro Browserinstanz immer genau eine Seite vorgerenderet, aber nur wenn das letzte prerendern mindestens 0,5s zurück liegt.

Einen Einblick in die aktuelle Arbeit des Prerenderers erhält man unter chrome://net-internals/#prerenderer.

In einigen Fällen stoppt das Prerendern ohne weitere Meldung direkt wenn es dazu kommen könnte, dass der User inkorrekten Content sehen könnte. Hier eine Liste, ohne Anspruch auf Vollständigkeit:

  • ein Download wird initiiert
  • HTMLAudio oder Video in der Seite
  • ein POST, PUT oder DELETE in einem XMLHTTPRequest
  • HTTP Authentifizierung
  • HTTPS
  • Seiten die eine Malware-Warnung auslösen
  • ein Popup oder eine neue Window Erzeugung
  • sobald eine Webseite ungewöhnlich viele Ressourcen beansprucht
  • Plugins wie z.B. würden geladen

DNS-Prefetching, bzw. Pre-Resolving

DNS-Prefetching, also lediglich das vorab Auflösen der Hostnamen der später zu ladenden Ressourcen, hat mit Abstand das Beste Kosten-Nutzen Verhältnis. Dies wirkt sich umso stärker aus, je größer die Latenz der Verbindung ist, also am besten z.B. bei mobilen Anschlüssen.

Dies geschieht mit dem link type “dns-prefetch”. Chrome befrägt den DNS Server z.B. auch bereits bei der Eingabe der URL und holt auch bei jedem Start die Adressen der 10 zuletzt aufgerufenen Hosts.

Prefetching in Internet Explorer 9, Safari 5.0.1 und Opera 11

Zu guter Letzt noch ein Wort zu den anderen großen Browsern: seit der Version 9 kann auch der Browser aus dem Hause Microsoft DNS-Prefetching (verhält sich wie Chrome, löst zusätzlich pretch link-Elemente auf als wäre es dns-prefetch), ebenso wie Safari 5.0.1. Opera benutzt bisher noch kein DNS-Prefetching.

Byte Range Requests in Javascript machen

1. September 2011

Nach der kürzlich bekannt gewordenen Apache Lücke, die den Webserver durch besondere Byte Range Requests aus dem Tritt bringt und den Server gleich mit lahmlegt, habe ich mir diese 206er Server-Antworten genauer angesehen.

Das MP3 Projekt jPlayer hat mich diesbezüglich mit am meisten beeindruckt (seeking direkt im Browser!) und daher fand ich es spannend diese Requests in Javascript abzubilden, wie es bereits in vielen Webanwendungen die mit Streaming arbeiten zum Einsatz kommt.

Nach ein wenig Recherche bei den Apache Labs und ein wenig RFC Lesen ergab sich der zugehörige jQuery Code fast von selbst:

Inhalt der Datei “content”:

12345678901234567890123456789012345678901234567890
12345678901234567890123456789012345678901234567890
12345678901234567890123456789012345678901234567890

Und hier der zugehörige JS-Teil:

<script>
_byte_range_request =  {
  make_request : function() {
    $.ajax({
      type: "GET",
      url : 'content',
      beforeSend: function (XHR) {
        bytes = "bytes=0-2";
        XHR.setRequestHeader('Range', bytes);
      },
      complete: function (XHR, textStatus) {
        $('#target').html(XHR.responseText);
      }
    });
  }
};
(function(_x) {
  _x.make_request();
})(_byte_range_request);
</script>

Anstelle der kompletten Datei werden in diesem Fall nur die ersten drei Bytes geladen und der String “123″ in das #target DIV geschrieben. Eine Live-Demo ist hier zu finden.

CSS: reset vs. normalize

3. Juli 2011

Ende Juni 2011 hat sich beim breit unterstützten (und sehr interessanten) Projekt HTML5 Boilerplate hinsichtlich der mitglieferten CSS-Definitionen einiges grundlegend geändert. HTML5 Boilerplate versucht dem Benutzer ein Grundgerüst von HTML/CSS/Javascript/Server-Config Dateien für Webprojekte an die Hand zu geben, die bereits für gängige Performanceoptimierungen und Best-Practices vorbereitet sind.

Zuvor setzten die HTML5 Pioniere um Paul Irish auf eine bewährte Techink, bei der alle Standard-CSS-Attribute der verschiedenen Browser einheitlich zurückgesetzt wurden. Eine ausgezeichnete Lösung hierfür gibt es von Eric Meyer.

Man könnte es auch so Formulieren: mit einem Radlader und einer Walze wird alles eingeebnet, um ein solides Fundament für kommende Arbeiten zu schaffen. Dies verlangt vom Designer dann anschließend alle Werte neu zu setzen, und leider auch triviale wie z.B.

strong { font-weight: bold; }

Einen filigraneren Ansatz hingegen verfolgt das von HTML5-Boilerplate nun verwendete normalize.css von Nicolas Gallagher and Jonathan Neal. Sie versuchen alle Werte in allen Browsern einheitlich zusetzen, allerdings bereits mit für viele Projekte passenden Voreinstellungen (Demo Seite). Hier kann (und sollte) man einen Blick drauf werfen.

Dieser Ansatz spart sehr viele Redefinitionen und damit natürlich auch viele Bytes die nicht übertragen und vom Browser nicht interpretiert werden müssen (auch wenn es gute CSS Kompressoren gibt, wie z.B. YUI Compressor und minify). In den diversen Entwicklertools sieht es denn auch deutlich aufgeräumter aus, den auch hier bringen weniger Style-Defintionen eine bessere Lesbarkeit mit sich.

Bei der Browser-Unterstützung werden ebenfalls keine halben Sachen gemacht: Chrome, Firefox 3+, Safari 4+, Opera 10+, Internet Explorer 6+.

Bei der großen Verbreitung von HTML5-Boilerplate und der breiten Unterstützung die dieses Projekt von Brachenriesen erfährt, kann es gut sein, dass normalize.css ein wichtiger erster Ansatzpunkt bei neuen Vorhaben werden kann und bald mehr und mehr im Netz zu finden sein wird.

Das Laden einer Webseite in den Browser

26. Juni 2011

Allgemeines

Im letzen Blog-Artikel ging es um “Flüssige Webanwendungen – Reflows vermeiden“. Den Schritt dort zum Erzeugen des DOM-Baumes schauen wir uns heute etwas genauer an: das Parsen also das grammatikalische Analysieren eines oder mehrerer zusammengehörender Dokumente um daraus eine komplexe Datenstruktur (bzw. exakt gesehen den Zugriff auf diese Datenstuktur), das Document Object Model (DOM) zu erzeugen, welches beim späteren Rendern, sprich Zeichnen, der Webseite neben dem CSS Object Model (CSSOM) eine der beiden Grundlagen ist. Die konkreten Erläuterungen beziehen sich auf HTML5; für XHTML bzw. HTML4 und kleiner gelten z.T. stark abweichende Parser.

Ausschlaggebend für mein Interesse daran war zunächst der Artikel “HTML5 Standard – Parsing HTML documents” bzw. natürlich auch “How a web page loads” von Tony Gentilcore.

Nun zu den Details

Als erstes muss das jeweilige Character Encoding, also der richtige Zeichensatz des Input Streams ermittelt werden. Hierzu kann der Parser mehrere Hinweise erhalten:

  • explizites Überschreiben auf Userwunsch direkt im Browser
  • Hinweis im HTTP Header
  • Hinweis beim Preparsing in den ersten 1024 Byte, META-Tag
  • durch einen Scan des Dokumentes
  • durch einen früheren Besuch der Seite
  • durch ein gesetztes Locale oder durch die Netzwerkumgebung

Das so genannte “Tokeniser”-Subsystem (oder auch Preprocessing) bekommt als erstes alle Teile der verschiedenen Quellen, sobald sie vom Browser geladen (Netz, Festplatte, Cache, …) und ggf. richtig konvertiert wurden; er bereitet sie für den eigentlichen Parser vor, in dem er sie in so genannte “Tokens” teilt, das sind z.B. Start- und End-Tags, aber auch alle sonstigen Zeichen, diese jeweils einzeln.

Der “Tree Builder” baut nun aus den Tokens den eigentlichen Baum, in dem er Tags an den richtigen Stellen einfügt (bzw. z.B. bei Javascript ggf. natürlich auch löscht). Fehlende Tags (html, head, body, …) ergänzt er ggf. genau wie fehlende schließende Tags. Wird beim Einfügen eines Elementes in den DOM Baum eine weitere Ressource (z.B. externe Javascript Datei, ein Stylesheet oder ein Bild) benötigt, wird nun das Nachladen dieser veranlasst.

 

grobe Skizze wie der Parser aus dem HTML den DOM-Baum generiert

HTML to DOM Parsing Model

Beim Übersetzen des HTMLs in den DOM-Baum gibt es verschiedene besondere Situation, so z.B. wenn der Browser ein <script> Tag findet. Um ein Skript zu interpretieren muss es vollständig geladen und alle CSS-Informationen müssen vorhanden sein (da das Javascript vllt. einzelne Informationen hier lesen oder schreiben möchte). Das Skript muss direkt interpretiert werden, da ein document.write() genau an der vorgesehenen Stelle schreiben können muss. Hier können Entwickler durch die Attribute “defer” oder “async” sagen, dass dieser Script-Block auch zu einem späteren Zeitpunkt interpretiert werden kann, ansonsten wartet der Parser mit dem Weitermachen, bis der komplette Teil interpretiert worden ist (siehe auch Erläuterungen in diesem Artikel (bei webkit.org) und bei “Best Practices for Speeding Up Your Web Site”, die Abschnitte “Put Stylesheets at the Top” and “Put Scripts at the Bottom”).

In Webkit z.B. gibt es eine Optimierung derart, dass ein so genannter Preload Scanner den Code weiter analysiert und versucht weitere externe Elemente zu identifizieren, deren Ladevorgang er dann schon anweisen kann; andere Browser haben ähnliche Ansätze dafür, so dass ein Skript Block zwar weiterhin ein Blockieren auslösen kann, dies aber weniger schlimm (im Sinne von Zeitoptimierungen) für den Anwender ist.

Direkt nach dem Parsen werden alle Skripte geladen und interpretiert, die durch die Attribute “defer” oder “async” noch ausstehen. Wenn alle diese Skripte abgearbeitet wurden wird der DOMContentLoaded-Event ausgelöst. Wenn auch alle ausstehenden Content-Elemente (wie z.B. Bilder) geladen wurden, wird der load-Event im Kontext window ausgelöst, die Webseite ist nun fertig im Browser geladen.

Reflows bewusst vermeiden

20. Juni 2011

Das Ziel: Flüssige Webanwendungen

 

In einem Video von Paul Irish über DOM, HTML5, & CSS3 Performance ging es im ersten Teil um das Rendern, also das Zeichnen von Webseiten anhand des DOM-Baumes und den Style-Informationen.

Beim Webseiten-Layout hat man nur wenig Einfluß darauf, wann dieser Render-Tree, also die zugehörige Datenstruktur neu berechnet werden muss, außer wenn man die Reihenfolge der Elemente so wählt, dass es beim Aufbau der Seite zu möglichst wenigen nachträglichen Verschiebungen kommt. Weit mehr Einfluß hat man hingegen bei nachträglichen Veränderungen z.B. mittels Javascript.

Er nahm Bezug auf einen Artikel von Tony Gentilcore, welcher sich z.T. wieder auf Grundlagen von Stoyan Stefanov bezog (die Grafik ist wirklich toll für das Verständnis).

Das durch Veränderung bzw. das Schreiben von Werten, welche die gezeichneten Elemente verändern (z.B. width und height), ein Neuzeichnen notwendig wird, ist nach kurzem Überlegen schnell klar. Nicht so klar ist es hingegen vielleicht, weshalb auch beim Lesen von Werten eine Neuberechnung des Layouts notwendig werden kann.

Um das zu verstehen bedarf es einiger Kenntnisse über die Schritte, die im Browser (hier insbesondere die Webkit Engine) zum Anzeigen einer Webseite führen. Eine mögliche Optimierungsstrategie häufiges Neuzeichnen zu vermeiden ist es, mehrere solcher Reflow Schritte zu sammeln und sie auf einmal abzuarbeiten. Wird aber nun lesend auf die Datenstruktur bzw. den Baum zugegriffen, müssen alle Schritte in der Warteschlange abgearbeitet werden um den derzeit aktuellen Wert zu berechen und dann auslesen zu können.

Hier nun seine umfangreiche, wenn auch vielleicht nicht vollständige Liste; in jedem Fall aber ein guter Anhaltspunkt um ein Gefühl dafür zu bekommen, worum es geht:

 

Element

clientHeight, clientLeft, clientTop, clientWidth, focus(), getBoundingClientRect(), getClientRects(), innerText, offsetHeight, offsetLeft, offsetParent, offsetTop, offsetWidth, outerText, scrollByLines(), scrollByPages(), scrollHeight, scrollIntoView(), scrollIntoViewIfNeeded(), scrollLeft, scrollTop, scrollWidth

Frame, Image
height, width

Range
getBoundingClientRect(), getClientRects()

SVGLocatable
computeCTM(), getBBox()

SVGTextContent
getCharNumAtPosition(), getComputedTextLength(), getEndPositionOfChar(), getExtentOfChar(), getNumberOfChars(), getRotationOfChar(), getStartPositionOfChar(), getSubStringLength(), selectSubString()

SVGUse
instanceRoot

window
getComputedStyle(), scrollBy(), scrollTo(), scrollX, scrollY, webkitConvertPointFromNodeToPage(), webkitConvertPointFromPageToNode()