HTML5-WPO-Slides



HTML5-WPO-Slides

0 1


HTML5-WPO-Slides

Slidedeck for the HTML5 Days 2014 in Munich

On Github Schepp / HTML5-WPO-Slides

Möglichkeiten und Fallstricke bei der Performance-Optimierung mit HTML5

Scriptloading

(20)

Defer

<script src="independantscript.js" defer></s​cript>

Das defer-Attribut ist eine Erfindung von Microsoft und kann an script-Elemente angeheftet werden.

Dieses Script wird nicht mehr an Ort und Stelle ausgeführt, sondern erst wenn es heruntergeladen und das HTML-Dokument fertiggeparsed ist. Es blockiert damit nicht mehr den HTML-Parser.

Normalerweise blockiert der Parser, denn es könnte ja ein document.write im Script vorkommen. Bei defer wird jegliches document.write ignoriert.

Defer

<script src="independantscript.js" defer></s​cript>

defer funktioniert nur für extern referenziertes JavaScript. Inline-JavaScript wird immer an Ort und Stelle ausgeführt.

defer kann nicht mit dynamisch per JavaScript ins DOM eingehängten Scripten benutzt werden.

Ein in HTML mit defer ausgestattetes Script verzögert wie gehabt das DOMContentLoaded-Event und auch das globale load-Event.

Defer

Der Browser führt alle mit defer markierten Scripte in der im Markup definierten Reihenfolge aus. Sie dürfen also Abhängigkeiten untereinander aufweisen:

<script src="jquery.js" defer></s​cript>
<script src="jquery-plugin.js" defer></s​cript>

Defer

Ein Mischmasch ist jedoch keine so gute Idee:

<script src="jquery.js" defer></s​cript>
<script src="jquery-plugin.js"></s​cript>

Defer

Desktop:

✔ ✔ ✔ 4+*/10+

*= bei IE < 10 noch hier und da nicht ganz ideal, weil die Reihenfolge "aufbricht", sobald ein Script frühzeitig anfängt, das DOM zu manipulieren.

Presto-Opera unterstützt das defer-Attribut nicht, Blink-Opera (15+) jedoch schon.

Defer

Mobile:

3+ ✔ ✔ ✔ 10+

Presto-Opera unterstützt das defer-Attribut nicht, Blink-Opera (16+) jedoch schon.

Async

<script src="independantscript.js" async></s​cript>

Das async-Attribut ist eine Erfindung von Mozilla und kann ebenfalls an script-Elemente angeheftet werden.

Dieses Script muss nicht mehr an Ort und Stelle, und auch nicht "in Reihe" ausgeführt werden, sondern kann ausgeführt werden, sobald es heruntergeladen ist. Es blockiert damit weder den HTML-Parser, noch anderes Javascript.

Normalerweise werden Scripte zwar durchaus parallel geladen, dann aber in Reihe ausgeführt, da untereinander Abhängigkeiten bestehen könnten.

Async

<script src="independantscript.js" async></s​cript>

async funtioniert nur für extern referenziertes JavaScript. Inline-JavaScript wird immer in Reihe ausgeführt!

Ein in HTML mit async ausgestattetes Script verzögert nicht mehr das DOMContentLoaded-Event, wohl aber das globale load-Event.

Async

Keine gute Idee:

<script src="jquery.js" async></s​cript>
<script src="jquery-plugin.js" async></s​cript>

Da jquery-plugin.js sehr wahrscheinlich kleiner ist als jquery.js wäre es vorher runtergeladen und würde als erstes ausgeführt. Es würde Schiffbruch erleiden.

Async

Besser:

<script src="jquery-und-alle-jquery-plugins.js" async></s​cript>
<script src="von-jquery-vollkommen-unabhaengiges-script.js" async></s​cript>

Die Chancen für async steigen mit dem Grad, in dem wir uns von jQuery unabhängig machen (mehr zu dem Thema später).

Async

Scripte werden von Haus asynchron ausgeführt, wenn Sie dynamisch ins DOM gehängt werden:

(function() {
    var script = document.createElement('script');
    script.src = "file.js";
    document.body.appendChild(script);
})()

Firefox 3.6 und brauchte aufgrund eines Bugs die explizite Anweisung script.async = true;.

Async

Soll ein dynamisch erzeugtes Script explizit nicht asynchron ausgeführt werden:

(function() {
    var script = document.createElement('script');
    script.src = "file.js";
    script.async = false;
    document.body.appendChild(script);
})()

Async

Vorsicht bei async und folgenden Konstruktionen:

document.addEventListener('DOMContentLoaded', function() {
    console.log('DOMContentLoaded');
});
window.addEventListener('load', function() {
    console.log('load');
});

Async

Besser abfragen, ob das wir das Event nicht vielleicht schon verpasst haben:

if ( document.readyState === 'interactive' ) {
    console.log('DOMContentLoaded');
} else {
    document.addEventListener('DOMContentLoaded', function() {
        console.log('DOMContentLoaded');
    });
}
if ( document.readyState === 'complete' ) {
    console.log('load');
} else {
    window.addEventListener('load', function() {
        console.log('load');
    });
}

(oder jQuerys $(document).ready() & Co nutzen)

Async

Desktop:

✔ ✔ ✔ 10+

Presto-Opera unterstützt das async-Attribut weder in HTML noch in dynamisch erzeugten Scripten. Blink-Opera (15+) unterstützt es.

Async

Mobile:

3+ ✔ ✔ ✔ 10+

Presto-Opera unterstützt das async-Attribut weder in HTML noch in dynamisch erzeugten Scripten. Blink-Opera (16+) unterstützt es.

IE-Spezialtrick: Asynchrones Script-Parsing

Beim dynamischen Einfügen von Scripten ins DOM wird ein Script normalerweise erst in folgendem Augenblick heruntergeladen:

document.body.appendChild(script);

IE (6+) lädt das Script allerdings schon in diesem Moment:

script.src = "file.js";

Auf die Weise lässt sich in IE ein Script schon vorzeitig laden, so dass es bei Bedarf schneller zu Hand ist.

Weiterführende Literatur

HTML5 Rocks - Script Loading

Die Zukunft

Der geplante EcmaScript 6 Module Loader wird im Browser asynchron arbeiten (anders als z.B. in Node.js) und dabei trotzdem Modulabhängigkeiten sicherstellen, ähnlich wie es require.js & Co heute schon tun:

/* mymodule.js */
export class q {
    constructor() {
        console.log('this is an es6 class!');
    }
}
<script type="module">
    // loads the 'q' export from 'mymodule.js' in the same path as the page
    import { q } from 'mymodule';
    new q(); // -> 'this is an es6 class!';
</s​cript>

Siehe ES6 Modules Polyfill

Die Zukunft

Die Web Components bringen ein Feature namens HTML Imports mit, das ebenfalls asynchron sein, und Modulabhängigkeiten sicherstellen kann:

<head>
    <link rel="import" href="flexslider.html" async>
    <link rel="import" href="fancybox.html" async>
</head>
<!-- flexslider.html -->
<link rel="import" href="jquery.html">
<script src="js/flexslider.js"></s​cript>
<!-- fancybox.html -->
<link rel="import" href="jquery.html">
<script src="js/jquery.fancybox.js"></s​cript>
<!-- jquery.html -->
<script src="js/jquery.js"></s​cript>

Vorladen, Vorrendern & Nachladen

(32)

Prefetching

<link rel="next" href="./gallery.html">
<link rel="prefetch" href="./js/image-carousel.js">
<link rel="prefetch" href="./images/picture-1.jpg">
<link rel="prefetch" href="./images/picture-2.jpg">
  • Die angegebene Ressource wird bei eintretendem Leerlauf vorgeladen
  • In der Ressource verlinkte Subressourcen werden nicht beachtet (z.B. in HTML oder CSS)
  • Chrome und IE11 machen bis zu 10 Requests parallel, Firefox nur einen
  • Ideal zum Vorbereiten der nächsten Seite (Cache-Header nicht vergessen!)

Prefetching

Desktop:

✔* ✘ ✔ 11+

*= Aktuell nur via --prerender=enabled Start-Parameter Soll demnächst offiziell freigeschaltet werden

Testseite

Prefetching

Mobile:

4+ ✘ ✘ 24+ 11+? (WP8.1)

Testseite

DNS-Prefetching

<link rel="dns-prefetch" href="//ajax.googleapis.com">
  • Der angegebene Hostname wird so schnell wie möglich per DNS-Abfrage aufgelöst und gecached
  • Das spart auf dem Desktop im Schnitt 125 ms. bei der späteren Anfrage einer Ressource von diesem Host
  • Auf mobilen Devices spart es deutlich mehr Zeit

Für durchschnittliche DNS-Zeiten, siehe chrome://histograms/DNS -> Sektion DNS.PrefetchResolution

DNS-Prefetching

Gut zum Vorbereiten des Browsers auf externe JavaScript-Libraries:

<head>
    <link rel="dns-prefetch" href="//ajax.googleapis.com">
</head>
<body>
    ...blah...blah...
    <script src="//ajax.googleapis.com/ajax/libs/jquery/1.10.2/jquery.min.js"></s​cript>
</body>

DNS-Prefetching

Hilfreich bei externen Webfonts, sofern diese erst im Stylesheet referenziert werden:

<head>
    <link rel="stylesheet" href="styles.css">
    <link rel="dns-prefetch" href="//fonts.googleapis.com">
</head>
@import url(//fonts.googleapis.com/css?family=Open+Sans);

(Noch besser ist es, Webfonts gar nicht via CSS zu importieren)

DNS-Prefetching

Hilfreich bei weiterleitungen:

<link rel="dns-prefetch" href="//mobil.zeit.de">

http://www.zeit.de/index -> Weiterleitung -> http://mobil.zeit.de/index

(Noch besser ist es, auf Weiterleitungen zu verzichten)

DNS-Prefetching

Desktop:

✔ ✘ ✔ 11+

Testseite

DNS-Prefetching

Mobile:

✘ 29+ ✘ 24+ 11+? (WP8.1)

Testseite

Subresource

<head>
    <link rel="subresource" href="veryimportant.js">
</head>
<body>
    ...blah...blah...
    <script src="veryimportant.js"></s​cript>
</body>
  • Die angegebene Ressource wird früh beim Preparser registriert
  • Der Preparser kann sie vorladen, abhängig von ihrer vermuteten Rolle im "Critical Path"
  • Erlaubt den Spagat aus "JavaScript nicht-blockierend im Fuß" + frühes Signalisieren an den Preparser

Subresource

Desktop:

✔ ✘ ✘ 11+

Subresource

Mobile:

✘ ✔ ✘ ✘ 11+? (WP8.1)

Prerender

<link rel="prerender" href="probable-next-page.html">
  • Kann in jeder Seite nur einmal verwendet werden
  • Öffnet eine unsichtbare Browser-Instanz und rendert die angegebene Seite darin
  • Surft der Besucher dann zu dieser Seite weiter, ist sie sofort da

Prerender

Prerender

Aktives Prerender wird im Chrome Task Manager (Shift + Esc) angezeigt:

Prerender

Folgende Bedingungen müssen erfüllt sein:

  • Der Browser hält gerade keine andere Seite via "prerender" vor
  • Die Ziel-URL fordert nicht zum Download auf
  • Die Ziel-URL erzeugt keine alerts oder neue Fenster
  • Die Ziel-URL ist nicht passwortgeschützt (HTTP Auth)
  • Es befinden sich weder HTML5 Video/Audio noch Flash in der zu rendernden Seite
  • Der Browser befindet sich nicht im Inkognito-Modus
  • Die Developer Tools sind nicht geöffnet

Prerender

Achtung beim Zählen von Seitenaufrufen!

  • Am besten nicht per Bild, sondern per JavaScript
  • Vor dem Tracken die Page Visibility API befragen
  • Falls die Seite geprerendert wird, auf einen Sichtbarkeitswechsel horchen:
if (document.visibilityState == 'prerender') {
  document.addEventListener('visibilitychange', handleVisibilityChange, false);
}

Wird von Google Analytics & Co schon lange beachtet

Prerender

Achtung bei zeitbasierten JavaScript-Methoden!

  • requestAnimationFrame statt setTimeout oder setInterval
  • CSS-basierte Animationen statt JavsScript-basierte

Diese Techniken werden in Hintergrund-Tabs pausiert und fressen dann keine Ressourcen.

Prerender

Desktop:

✔ ✘ ✘ 11+

Prerender

Mobile:

✘ ✘ ✘ ✘ 11+? (WP8.1)

Bei Mobilgeräten gibt es den Interessenskonflikt, dass man gleichzeitig den Datenverbrauch gering halten möchte. Dementsprechend bleibt prerender in Chrome abgeschaltet.

HTTP Header

Alle <link>-Anweisungen sollen sich zukünftig auch in HTTP-Header verlagern lassen:

HTTP/1.1 200 OK
Date: Thu, 26 Jul 2012 22:27:21 GMT
Server: Apache
Content-Location: foo.html
Vary: negotiate,Accept-Encoding
Last-Modified: Thu, 26 Jul 2012 20:55:56 GMT
Accept-Ranges: bytes
Content-Length: 675
Expires: Thu, 02 Aug 2012 22:27:21 GMT
Link: <js/nextpage.js>; rel=prefetch
Link: <js/logic.js>; rel=subresource
Content-Type: text/html; charset=utf-8

Wird aktuell nur von Firefox unterstützt.

Lazyload

<head>
     <link rel="stylesheet" src="styles.css">
     <link rel="stylesheet" src="animations.css" lazyload>
 </head>
 <body>
    <img src="logo.png">
    <img src="header.png">
    <img src="additionalImages1.png" lazyload>
    <img src="additionalImages2.png" lazyload>
 </body>
  • Eine mit lazyload ausgezeichnete Ressource wird im Ladevorgang hinten angestellt
  • Eine mit lazyload ausgezeichnete Ressource blockiert nicht mehr das globale load-Event

Lazyload

Desktop:

✘ ✘ ✘ 11+

Lazyload

Mobile:

✘ ✘ ✘ ✘ 11+? (WP8.1)

Postpone

<img src="image.png" postpone>

Eine mit postpone ausgezeichnete Ressource wird erst geladen, sobald sie sichtbar wird:

  • Sei es durch explizites Einblenden (z.B. via display != none) im Sichtbereich
  • Oder durch ein in-den-Sichtbereich-Scrollen

Eine mit postpone ausgezeichnete Ressource blockiert ebenfalls nicht mehr das globale load-Event

Postpone

Desktop:

✘ ✘ ✘ ✘

Postpone

Mobile:

✘ ✘ ✘ ✘ ✘

Lazyload & Postpone

lazyload und postpone können an folgende Element-Typen angeheftet werden:

  • img
  • audio
  • video
  • script
  • link
  • embed
  • iframe
  • object

Lazyload & Postpone

  • lazyload- und postpone-behaftete Elemente blockieren das globale load-Event nicht.
  • Sind alle mit lazyload ausgezeichneten Elemente geladen, wird ein globales lazyload-Event gefeuert
  • lazyload- und postpone-behaftete Elemente feuern nach wie vor individuelle, eigene load-Events

XHR zum Vorladen verwenden?

Nope! XHR-Calls sollten nicht für das Vorladen kritischer Ressourcen genutzt werden, da sie eine deutlich geringere Priorität genießen als via <link> deklarierte Ressourcen.

Die Zukunft

Für die nahe Zukunft sind folgende zusätzliche Link-Anweisungen geplant:

  • <link rel="preconnect"> öffnet frühzeitig eine TCP-Verbindung zu dem angegebenen Host, um den sogenannten TCP-Slow-Start zu verstecken.
  • <link rel="preload"> ist eine Kombination aus subresource & prefetch mit "content awareness".

Siehe Diskussion auf lists.w3.org

Die Zukunft

Der geplante EcmaScript 6 Module Loader wird dem Browser möglicherweise ebenfalls Hinweise bzgl. zu ladender JavaScript-Module geben:

<script type="module">
    // loads the 'q' export from 'mymodule.js' in the same path as the page
    import { q } from 'mymodule';
</s​cript>

Siehe ES6 Modules Polyfill

jQuery durch native Techniken ersetzen

(21)

jQuery-Selector

var el = $('selector');

var $ = document.querySelectorAll.bind(document);

var el = $('#id')[0],
    els = $('.class');

els.forEach( function(el, index) {
    el.innerHTML = 'Number ' + (index + 1);
});

jQuery-Events

$('selector').on('click', handleEvent);

Element.prototype.on = Element.prototype.addEventListener;

$('selector')[0].on('click', handleEvent);

jQuery-Events

$('selector').on('click', 'a[href="#"]', handleEvent);

Element.prototype.on = function() {
    var args = arguments;
    if ( args.length <= 2 ) {
        this.addEventListener(args[0], args[1]);
    } else {
        this.addEventListener(args[0], function(e) {
            if ( e.target.matches(args[1] ) {
                args[2](e);
            }
        });
    }
};

$('selector')[0].on('click', 'a[href="#"]', handleEvent);

jQuery-Events

$('selector').on('touchstart click', 'a[href="#"]', handleEvent);

Element.prototype.on = function() {
    var args = arguments,
        that = this;
    args[0].split(' ').forEach( function(eventName) {
        if ( args.length <= 2 ) {
            that.addEventListener(eventName, args[1]);
        } else {
            that.addEventListener(eventName, function(e) {
                if ( e.target.matches(args[1] ) {
                    args[2](e);
                }
            });
        }
    });
};

$('selector')[0].on('touchstart click', 'a[href="#"]', handleEvent);

querySelectorAll & addEventListener & matches

Desktop:

✔ ✔ ✔ 9+

element.matches()/matchesSelector() Doku & Supporttable

querySelectorAll & addEventListener & matches

Mobile:

2.2+ ✔ ✔ ✔ 9+

jQuery-AJAX

$.getJSON('/my/url', function(data) {});

var $getJSON = function( url, callback ) {
    var request = new XMLHttpRequest;
    request.open('GET', url, true);
    request.onload = function() {
      if ( request.status >= 200 && request.status < 400 ){
        callback(JSON.parse(request.responseText));
      } else {
        console.log('serverside error fetching ' + url);
      }
    };
    request.onerror = function() {
        console.log('clientside error fetching ' + url);
    };
    request.send();
}

$getJSON('/my/url', function(data) {});

XMLHttpRequest & JSON

Desktop:

✔ ✔ ✔ 8+

XMLHttpRequest & JSON

Mobile:

2.1+ ✔ ✔ ✔ 9+

jQuery-Klassensetzen

$('selector').addClass('classA classB');

['add', 'remove', 'toggle'].forEach(function (mode) {
    Element.prototype[mode + 'Class'] = function (classes) {
        var that = this;
        classes.split(' ').forEach(function (currentclass) {
            that.classList[mode](currentclass);
        });
    };
});

var el = $('selector')[0];
el.addClass('classA classB');

Demo

forEach & classList

Desktop:

✔ ✔ ✔ 10+

Can I use classList

forEach & classList

Mobile:

3+ ✔ ✔ ✔ 10+

*= Android 2.x - 3.x können prinzipiell CSS Animationen, haben aber zahlreiche Bugs

Can I use classList

jQuery-Effekte

$(el).slideDown();

el {
    display: none;
}
.slide {
    display: block;
    transition: max-height 300ms;
    overflow: hidden;
}
.slideDown {
    max-height: 500px;
}
Element.prototype.slideDown = function() {
  this.style.display = '';
  this.addClass('slide slideDown');
};

el.slideDown();

jQuery-Effekte

$(el).slideUp();

.slide {
    display: block;
    transition: max-height 300ms;
    overflow: hidden;
}
.slideUp {
    max-height: 0;
}
Element.prototype.slideUp = function() {
  this.on('transitionEnd', function(e) {
      e.target.style.display = 'none';
  });
  this.addClass('slide slideUp');
};
el.slideUp();

jQuery-Effekte

$(el).fadeIn();

el {
    display: none;
}
@keyframes fadeIn {
    from { opacity: 0; }
    to { opacity: 1; }
}
.fadeIn {
    display: block;
    animation: fadeIn 300ms;
}
Element.prototype.fadeIn = function() {
  this.addClass('fadeIn');
};

el.fadeIn();

jQuery-Effekte

$('<el/>').appendTo('body').fadeIn();

@keyframes fadeIn {
    from { opacity: 0; }
    to { opacity: 1; }
}
el {
    animation: fadeIn 300ms;
}
var el = document.createElement('el');
el.innerHTML('Elementinhalt');
document.body.appendChild(el);

jQuery-Effekte

$(el).fadeOut(300, function() { $(this).remove(); });

@keyframes fadeOut {
    from { opacity: 1; }
    to { opacity: 0; }
}
.fadeOut {
    animation: fadeOut 300ms;
}
Element.prototype.remove = function() {
    this.parentNode.removeChild(this);
};
Element.prototype.fadeOut = function() {
  this.on('animationend', function(e) {
      e.target.remove();
  });
  this.classList.add('fadeOut');
};
el.fadeOut();

jQuery-Effekte

Demo A

Demo B

Demo C

Animations & Transitions

Desktop:

✔ ✔ ✔ 10+

Can I use CSS Animation / CSS Transition / classList

Animations & Transitions

Mobile:

4+* ✔ ✔ ✔ 10+

*= Android 2.x - 3.x können prinzipiell CSS Animationen, haben aber zahlreiche Bugs

Can I use CSS Animation / CSS Transition / classList

Weiterführende Literatur

Klügeres Caching via HTML5 APIs

(47)

Motivation

"Cache Pinning"

OS Browser Max Persistent Cache Size iOS 4.3 Mobile Safari 0 iOS 5.1.1 Mobile Safari 0 iOS 5.1.1 Chrome for IOS 200 MB+ Android 2.2 Android Browser 4 MB Android 2.3 Android Browser 4 MB Android 3.0 Android Browser 20 MB Android 4.0 – 4.1 Chrome for Android 85 MB Android 4.0 – 4.1 Android Browser 85 MB Android 4.1 Firefox Beta 75 MB Blackberry OS 6 Browser 25 MB Blackberry OS 7 Browser 85 MB

Quelle

Motivation

HTTP Archive Trends

Motivation

HTTP Archive Trends

Motivation

HTTP Archive Trends

AppCache

  • Eines der prominentesten neuen Features von HTML5
  • Offline Speichern beliebig vieler Ressourcen
  • Erlaubt es, zusammen mit Webstorage, WebSQL oder IndexedDB, eine Webanwendung offlinefähig zu machen

Demo

AppCache

Desktop:

✔ ✔ ✔ 10+

AppCache

Mobile:

2.1+ ✔ ✔ ✔ 10+

AppCache

Leider das Enfant Terrible von HTML5:

The Application Cache has skills we need, but if you asked him to paint your bathroom he'd somehow manage to flood your kitchen and break your TV in the process, and he wouldn't care.

Jack Archibald

AppCache

<html manifest="offline.appcache">

offline.appcache:

CACHE MANIFEST
http://ajax.googleapis.com/ajax/libs/jquery/1.10.2/jquery.min.js
assets/script/main.js
assets/style/main.css
assets/style/fonts/font.woff
assets/style/images/sprite.png

Die aufgelisteten Ressourcen werden alle offline gespeichert und ab dem nächsten Aufruf der referenzierenden HTML-Seite von Platte bezogen.

AppCache

<html manifest="offline.appcache">

Leider wird immer auch die referenzierende HTML-Datei offline gespeichert!

Der Browser stellt also in Zukunft immer dieselben Inhalte dar, sofern diese wie üblich im HTML stecken.

WTF?

AppCache

Weil die HTML-Datei mitgespeichert wird, bringt es auch nichts, wenn man mit klassischen Cachebusting-Methoden Updates erzwingen will:

<html manifest="offline.appcache?v=2">

Die Änderungen passiert online und nicht offline beim User. Bringt also nichts!

AppCache

Die einzige Möglichkeit, den Browser zum Aktualisieren von Ressourcen zu bewegen, ist die Manifestdatei inhaltlich zu verändern, z.B. durch eine integrierte Versionsnummer:

CACHE MANIFEST
# v2
http://ajax.googleapis.com/ajax/libs/jquery/1.10.2/jquery.min.js
assets/script/main.js
assets/style/main.css
assets/style/fonts/font.woff
assets/style/images/sprite.png

AppCache

Achtung: Niemals Far-Future Cache Header für die Manifest-Datei setzen, weil man sonst keine Möglichkeit mehr hat, Dateien zu aktualisieren.

Der Besucher bliebe in der Vergangenheit gefangen.

AppCache

Apropos Cache-Header: Der Application Cache ist eine zweite Cache-Layer über dem üblichen clientseitigen Cache. Daher ist es weiterhin sinnvoll, alle Ressourcen mit Far-Future Cache Headern auszustatten, so dass die Browser nur die wirklich aktualisierten Dateien beim Updaten des Manifests vom Server lädt.

AppCache

CACHE MANIFEST
# v2
http://ajax.googleapis.com/ajax/libs/jquery/1.10.2/jquery.min.js
assets/script/main.js
assets/style/main.css
assets/style/fonts/font.woff
assets/style/images/sprite.png

Der Browser checkt und verarbeitet das Manifest immer erst nach Ende des Seitenladens. Sprich, neue Dateien manifestieren sich erst beim übernächsten Aufruf.

AppCache

AppCache

Ressourcen, die nicht im Manifest stehen, im HTML/CSS/JS aber referenziert werden, werden bei aktivem Offline-Modus nicht mehr geladen - auch dann nicht, wenn eine Onlineverbindung zu ihnen besteht. Abhilfe schafft der NETWORK-Eintrag:

CACHE MANIFEST
# v1
assets/foo.bar

NETWORK:
*

AppCache

CACHE MANIFEST
# v1
assets/foo.bar

NETWORK:
*

Das funktioniert dummerweise nur mit der HTML-Datei selbst nicht!!! Sie ist der sogenannte "Masterindex", args...

AppCache

Lösung: Ein nahezu leeres HTML Grundgerüst verwenden und die Inhalte nachladen:

  • via XHR, oder
  • via XHR + localStorage, oder
  • via HTML Imports, oder
  • via beidem (Featuredetection)

AppCache + XHR

<!DOCTYPE html>
<html manifest="offline.appcache">
<head></head>
<body>
    <script>(function(){
        var request = new XMLHttpRequest;
        request.open('GET', 'import.html', true);
        request.onload = function() {
            document.body.innerHTML = request.responseText;
        };
        request.send();
    }());</s​cript>
</body>
</html>

Demo

AppCache + XHR + localStorage

(function(){
    var request = new XMLHttpRequest;
    request.open('GET', 'import.html', true);
    if(content = localStorage.getItem('import.html')) {
        document.body.innerHTML = content;
        request.onload = function() {
            localStorage.setItem('import.html', request.responseText);
        };
    } else {
        request.onload = function() {
            localStorage.setItem('import.html', request.responseText);
            document.body.innerHTML = request.responseText;
        };
    }
    request.send();
}());

Demo

AppCache + HTML Imports

<!DOCTYPE html>
<html manifest="offline.appcache">
<head>
    <link rel="import" href="import.html">
</head>
<body></body>
</html>
<div id="content"><p>Hello World!</p></div>
<script>(function(){
    var importDoc = document.currentScript.ownerDocument,
        content = importDoc.querySelector('#content').innerHTML;
    if (document.readyState === 'interactive') {
        document.body.innerHTML = content;
    } else {
        document.addEventListener('DOMContentLoaded', function(){
            document.body.innerHTML = content;
        })
    }
}());</s​cript>

Demo

AppCache + XHR + HTML Imports

<!DOCTYPE html>
<html manifest="offline.appcache">
<head>
    <link rel="import" href="import.html">
</head>
<body>
    <script>(function(){
        if(!('import' in document.createElement('link'))){
            var request = new XMLHttpRequest;
            ...
        }
    }());</s​cript>
</body>
</html>

Demo

Web Components

Desktop:

2014* ✘ 2014 ?

*= ab Chrome 31 in about:flags als Eintrag Enable HTML Imports freischaltbar

Web Components

Mobile:

? 2014 ✘ 2014 ?

AppCache

Nachteile von AppCache:

  • Änderungen erst beim übernächsten Besuch sichtbar
  • Risiko einer Disparität von Frontend- und Backend-Code
  • Kompletter Abbruch wenn eine im Manifest gelistete Datei nicht abrufbar ist (z.B. 404)
  • Kompletter Abbruch bei Änderungen im Manifest während der Abarbeitung
  • Referenzierende Datei lässt sich nicht ausklammern

AppCache

Noch was? Jepp! Firefox jagt dem Benutzer Angst ein, indem er beim ersten Besuch danach fragt, ob die Seite Daten offline speichern darf:

Einzige Abhilfe: Serverseitiges Useragent-Sniffing :(

AppCache

Weiterführende Literatur:

localStorage

Synchrone API zum Speichern von Key/Value-Paaren:

localStorage.setItem('key','value');
var value = localStorage.getItem('key');

localStorage.removeItem('key');
localStorage.clear(); // Leert ihn komplett

Verarbeitet eigentlich nur Strings...

localStorage

Kann via JSON-Codierung auch Arrays oder Objekte speichern:

var arr = [0, 1, 2],
    obj = { 'key1': 0, 'key2': 0 };

localStorage.setItem('arr',JSON.stringify(arr));
localStorage.setItem('obj',JSON.stringify(obj));

arr = JSON.parse(localStorage.getItem('arr'));
obj = JSON.parse(localStorage.getItem('obj'));

localStorage & JSON

Desktop:

✔ ✔ ✔ 8+

localStorage & JSON

Mobile:

✔ ✔ ✔ ✔ 9+

localStorage

Kann via base64-Codierung auch Binärdaten speichern, z.B. von Bildern:

var image = new Image();
image.onload = function() {
    var canvas = document.createElement('canvas');
    canvas.width = image.width;
    canvas.height = image.height;
    canvas.getContext('2d').drawImage(image, 0, 0);

    localStorage.setItem('image',canvas.toDataURL('image/jpeg'));

    var secondImage = new Image();
    secondImage.src = localStorage.getItem('image');
}
image.src = 'picture.jpg';

Canvas

Desktop:

✔ ✔ ✔ 9+

Canvas

Mobile:

✔ ✔ ✔ ✔ 9+

localStorage Quota

Browser KB Chrome 33 5.120 Chrome Mobile 32 5.120 Firefox 27 5.120 IE 11 4.883 Opera 19 5.120 Mobile Safari 7 2.560 Safari 7 2.560

Quelle

localStorage

Nicht nur nützlich fürs "Cache Pinning", sondern ideal zum Verringern von HTTP-Requests durch bedingtes Inlinen.

localStorage

Erstbesuch

Es werden alle Dateien als <style>- und <script>-Blöcke ge-inlined

<style data-id="a6g7h89f" data-modified="123456789">...</style>

Erstbesuch

Nach dem Seitenladen liest ein Script alle Blöcke einzeln aus

speichert sie im localStorage mit data-id als Key und dem Inhalt als Value setzt einen Cookie mit data-id als Key und data-modified als Value

localStorage

document.addEventListener('DOMContentLoaded', function() {
    var items = document.querySelectorAll('style[data-id],script[data-id]');
    items = Array.prototype.slice.call(items,0);
    items.forEach(function (item) {
        var key = item.getAttribute('data-id'),
            timestamp = item.getAttribute('data-modified'),
            value = item.textContent;

        localStorage.removeItem(key);
        localStorage.setItem(key, value);
        if (localStorage.getItem(key)) {
            document.cookie = key + '=' + timestamp;
            console.log('Added/updated ' + key + ' to localStorage');
        }
    });
});

localStorage

Nachfolgende Besuche

  • Existiert kein Cookie mit dem Key wird die Datei wieder ge-inlined
  • Existiert der Cookie, stimmt aber das Datum nicht mehr, wird die aktuelle Datei ge-inlined
  • Existiert der Cookie und stimmt auch das Datum, wird ein Script ausgegeben, das die Datei aus dem localStorage holt und ins DOM hängt

localStorage

foreach($files as $file) {
    $id = md5($file);
    if(!isset($_COOKIE[$id]) || $_COOKIE[$id] != filemtime($file)) {
        echo '<script data-id="'.$id.'"
        data-modified="'.filemtime($file).'">
        '.file_get_contents($file).'
        </s​cript>';
    }
    else {
        echo '<script>
            var script = document.createElement("script");
            script.textContent = localStorage.getItem("'.$id.'");
            document.body.appendChild(script);
        </s​cript>';
    }
}

localStorage

Vorteile:

  • Keine extra Requests für kritische Ressourcen
  • Dennoch kein dauerhaftes Aufblähen des HTMLs
  • Je mehr kleine Dateien, desto besser für spätere Updates
  • Möglichkeiten für echte Delta-Updates via Diff-Tools
  • Kontrollierbarer als der AppCache

localStorage

Demo

localForage

Tipp: localForage ist ein Tool von Mozilla, das die localStorage-API aufgreift und:

  • mit Callbacks und/oder Promises asynchron arbeitet
  • das Speichern intern auf WebSQL, IndexedDB oder localStorage verteilt
  • das Speichern von Objekten und Arrays ohne Umwandlung erlaubt

Gruppenarbeit?

Lust die Demo zu erweitern:

  • um das Speichern von Bildern?
  • oder um localForage?
  • oder mit Delta-Updates (schwer)

Zukunft: ServiceWorker

The Service Worker Specification

ETA: Anfang 2015

Perfekt auf die GPU zugeschnittenes Rendering

(52)

GPU? Was ist das?

Ergänzend zum Hauptprozessor, der Central Processing Unit, gibt es die Graphics Processing Unit, den Grafikprozessor. Manchmal auch General Processing Unit genannt, weil man der GPU auch andere Dinge als nur Grafik berechnen kann, z.B. via WebCL (siehe auch).

GPU

Quelle

GPU

Quelle

Renderingablauf

CPU: Layoutberechnung zur Ermittlung der Grundfläche CPU: Einteilung in 256 x 256 Pixel große Kacheln GPU: Bereitet leere Fläche mit ermittelten Ausmaßen vor CPU: "Paintet" alle Kacheln innerhalb des Viewports BUS: Fertige Kacheln wandern zur GPU (je 256 KB) GPU: Tapeziert leere Fläche mit empfangenen Kacheln

Demo

Vorbereitendes Rendering

Einmal erledigt bereitet der Browser diejenigen Bereiche vor, die bei einem Scrollvorgang als nächstes in den sichtbaren Bildausschnitt rücken würden.

Vorbereitendes Rendering

Wie viel er vorbereitet hängt von der freien Speicherkapazität des jeweiligen Geräts ab:

  • Die Fläche eines Full-HD Bildschirms frisst 8 x 5 Kacheln à 256 KB = 10 MB.
  • Die Fläche eines Retina-iPads frisst 8 x 6 Kacheln à 256 KB = 12 MB

Vorbereitendes Rendering

Geht der Speicher zur Neige, verwirft der Browser entfernt liegende Kacheln zuerst, z.B. weggescrollte.

Die GPU arbeitet intern mit texturierten 3D-Flächen

Scrolling

Das Scrolling übernimmt nun komplett die GPU, in dem Sie die 3D-Fläche herumschiebt.

Scrolling

Vorteile

  • 3D-Flächen Herumschieben ist eine Spezialität der GPU - es geht blitzschnell!
  • Vor allem eine mobile CPU wäre überfordert und wird komplett entlastet.

Scrolling

Nachteile auf Mobilgeräten

  • Während des Scrollens nimmt die GPU keine Paint-Updates entgegen (Animated GIFs bleiben z.B. stehen)
  • Da die CPU außen vor bleibt, empfängt sie keinerlei onscroll-Events von der GPU*

Kuriosum: Fixed positionierte Animated GIFs werden weiterhin animiert!

*= Chrome auf Android wählt einen Mittelweg und updated zwischendurch.

Scrolling

Da die CPU außen vor bleibt, empfängt sie keinerlei onscroll-Events von der GPU

Bei "klebenden" Menüs schafft die neue Eigenschaft position: sticky Abhilfe (vorher/nachher). Unterstützen bisher nur die Safaris.

Offcanvas Menüs

Offcanvas Menüs

  • Verstecktes Menü niemals auf display: none stellen, sonst kann es nicht vorgerendert werden.
  • Menü ausschließlich mit transform: translateX bewegen
  • Menü ausschließlich mit transition oder animation animieren
  • Menü von Anfang an mit backface-visibility: hidden in eine eigene Compositing Layer promoten

Transform

Menü ausschließlich mit transform: translateX bewegen

Durch transform: translateX kann es bewegt werden, ohne für den Rest der Seite einen "Reflow" auszulösen.

Transform

Transform/Opacity Animationen

  • Menü ausschließlich mit transform: translateX bewegen
  • Menü ausschließlich mit transition oder animation animieren

Transform/Opacity Animationen

Startet eine CSS Animation, die auf transform, opacity oder filter (außer dem Blur-Filter) abzielt, löst der Browser das Element von der aktuellen Zeichenebene aus, und erzeugt in der GPU eine zweite Ebene mit dem Element. Sie wird "in eine eigene Layer promoted".

Nach der Animation werden die Ebenen wieder vereint.

Transform/Opacity Animationen

Eine promotete Ebene kann einzeln sehr effizient in der GPU transformiert oder durchsichtig gemacht werden, ohne dass der Rest der Seite angefasst werden muss. Und ohne dass die CPU etwas damit zu tun hat (siehe vorher/nachher.

Compositing

Compositing (englisch für Zusammensetzung, Mischung) ist ein Begriff aus der Video- und Filmtechnik und findet in der Postproduktion eines Filmes als visueller Effekt Anwendung. Im Compositing werden zwei oder mehr voneinander getrennt aufgenommene oder erstellte Elemente zu einem Bild zusammengeführt. In der Computergrafik versteht man unter Compositing das Zusammenfügen mehrerer hintereinanderliegender Schichten eines Volumens.

Wikipedia

Kein aktives Compositing

Kein aktives Compositing

Aktives Compositing

Aktives Compositing

Compositing erzwingen

  • Menü von Anfang an mit backface-visibility: hidden in eine eigene Compositing Layer promoten

backface-visibility: hidden und transform: translateZ(0) zwingen ein Element immer in eine eigene Ebene.

Sinnvoll, wenn eine Layer später sowieso promoted wird.

Compositing

Eine promotete Ebene kann einzeln sehr effizient in der GPU transformiert [...] werden, [...] ohne dass die CPU etwas damit zu tun hat.

Gut bei Blockaden des UI-Threads (z.B. durch JavaScript)! (siehe vorher/nachher)

Compositing

Eine promotete Ebene kann einzeln sehr effizient in der GPU transformiert [...] werden, [...] ohne dass die CPU etwas damit zu tun hat.

Auch ein gutes Mittel gegen sogenannte "Paint Storms".

Beispielseite mit vielen "Paint Storms": thenextweb.com

Compositing

Nachteil: Jede promotete Layer verbraucht zusätzlichen Speicher, und zwar mindestens Pixel x Pixel x 4 Bytes (bei Retina 4 mal so viel)

Implizite Layer Promotion

  • Elemente mit position: fixed*
  • Elemente mit overflow-scrolling: touch
  • Elemente, die gelayerte Elemente überlappen
  • html/Root, Video, Canvas Elemente
  • iFrames

*= in Chrome nur auf HiDPI-Displays, wegen Schriftglättung

Position: fixed auf Chrome

You might wonder why we don’t automatically promote fixed position elements. The answer is: we do for high DPI screens, but we don’t for low DPI because we lose sub-pixel antialiasing on text, and that’s not something we want to by default. On high DPI screens you can’t tell, so it’s safe there.

Paul Lewis

Union of Damaged Regions

Wenn der Chrome-Browser mehrere fixed positionierte Elemente auf einer Seite antrifft, fasst er sie ohne extra Eingriff alle zu einer Layer zusammen.

Das kann dann sehr schlecht enden, wenn die Elemente weit auseinander stehen. Explizite Layer Promotion hilft.

Silver Bullet?

*, *:before, *:after {
    backface-visibility: hidden;
}

Niemals!

Reflows vermeiden

Reflows kosten richtig viel Zeit, da das gesamte Layout neu berechnet und gepainted werden muss.

Diese DOM-Methoden, angewendet auf die angegebenen Elemente erzeugen einen Reflow:

  • Elemente: clientHeight, clientLeft, clientTop, clientWidth, focus(), offsetHeight, offsetLeft, offsetParent, offsetTop, offsetWidth, scrollHeight, scrollLeft, scrollTop, scrollWidth
  • Bilder: height, width
  • window: getComputedStyle(), scrollBy(), scrollTo(), scrollX, scrollY

Reflows vermeiden

// Read
var h1 = element1.clientHeight;
// Write (invalidates layout)
element1.style.height = (h1 * 2) + 'px';

// Read (triggers layout)
var h2 = element2.clientHeight;
// Write (invalidates layout)
element2.style.height = (h2 * 2) + 'px';

// Read (triggers layout)
var h3 = element3.clientHeight;
// Write (invalidates layout)
element3.style.height = (h3 * 2) + 'px';

Schlecht!

Reflows vermeiden

// Read
var h1 = element1.clientHeight;
var h2 = element2.clientHeight;
var h3 = element3.clientHeight;

// Write (invalidates layout)
element1.style.height = (h1 * 2) + 'px';
element2.style.height = (h2 * 2) + 'px';
element3.style.height = (h3 * 2) + 'px';

Reads und Writes batchen: Perfekt!

Reflows vermeiden

// Read
var h1 = element1.clientHeight;

// Write
requestAnimationFrame(function() {
  element1.style.height = (h1 * 2) + 'px';
});

// Read
var h2 = element2.clientHeight;

// Write
requestAnimationFrame(function() {
  element2.style.height = (h2 * 2) + 'px';
});

Writes per requestAnimationFrame in die Zukunft verlegen: <3!

Reflows begrenzen

Reflow lassen sich mit Hilfe von "Layout Boundaries" begrenzen.

Layout Boundaries sind Elemente mit unverrückbarer Größe, die eine Reflow-Welle ähnlich wie ein Deich stoppen.

Layout Boundaries

Eingebettete SVGs bilden Layout Boundaries oder Elemente...

  • deren displayweder auf inline, noch auf inline-blockstehen
  • deren height nicht in Prozent angegeben ist
  • deren height gesetzt, aber nicht auto ist
  • deren width gesetzt, aber nicht auto ist
  • deren overflow gesetzt ist (scroll, auto, hidden)
  • die kein Kindelement einer Tabelle sind

Layout Boundaries

Tool zum Highlighten von Elementen mit "Layout Boundaries"-Potential:

Boundarizr

Paint Zeiten reduzieren

Nicht zu viele CSS3-Effekte einsetzen! Speziell in Kombination werden sie teuer.

Auch wenn es anachronistisch klingt: Im Zweifel besser Bilder verwenden.

Zukunft

Die Browserhersteller arbeiten an einer neuen CSS-Eigenschaft namens will-change, die den Browsern signalisiert, dass eine Eigenschaft sich um Lauf des Lebenszyklus' ändern wird:

html {
    will-change: scroll-position;
}
.itemlist {
    will-change: contents, scroll-position;
}
.animated {
    will-change: transform, opacity;
}

Denkarbeit gefällig?

Wie würdet Ihr diese Animation ruckelfreier machen?

Es gibt zwei Lösungen. Bonuspunkt für den, der beide Lösungen findet!

Weiterführende Literatur

SPDY aka HTTP/2

(17)

Eigenschaften von SPDY

  • Für alle Webtechnologien vollkommen transparent
  • Baut nur eine Verbindung zum Server auf
  • Multiplexed alle Dateitransfers durch die eine Verbindung (ähnlich wie MHTML)
  • Server und Client steuern, was wann durch die Verbindung geht
  • Header Kompression und Senden von Header-Diffs
  • Ermöglicht proaktives serverseitiges Pushing
  • Abbrüche zwischendrin sind möglich
  • Sendet Binärdaten: Kompakter & weniger fehleranfällig

Prioritized Streams

Der Client kann bestimmte Ressourcen mit höherer Priorität anfordern

Header Kompression

Google observed an ~88% reduction in the size of request headers and an ~85% reduction in the size of response headers after enabling compression. This amounted to a saving of between 45 and 1142 ms in the overall page load time.

Quelle

Encryption

Die Browser supporten nur verschlüsseltes SPDY:

  • um Proxies davon abzuhalten, in den Datenstrom einzugreifen
  • um das Sicherheitslevel im Netz zu erhöhen (flankierend zu CSP & Co)

Aushandlung via HTTPS

Läuft vollautomatisch!

Aushandlung via HTTP

GET /page HTTP/1.1
Host: server.example.com
Connection: Upgrade, HTTP2-Settings
Upgrade: HTTP/2.0
HTTP2-Settings: (SETTINGS payload in Base64)
HTTP/1.1 200 OK 3
Content-length: 243
Content-type: text/html
(... HTTP 1.1 response ...)
HTTP/1.1 101 Switching Protocols 4
Connection: Upgrade
Upgrade: HTTP/2.0
(... HTTP 2.0 response ...)

Quelle

SPDY Push

Hinfällige Konzepte

  • Image Spriting
  • Concatenation
  • Inline Resources
  • Domain Sharding

Serverside

SPDY clients consume one worker instead of six

Quelle

Serverside

HTTP HTTPS SPDY Maximum pages/s 16.3 at 120 users 15.9 at 120 users 98 at 777 users Response at 100 users 1.1s 1.3s 1.1s Response at 120 users 1.4 s 1.5s 1.1s Response at 200 users 7.1s 7.8s 1.1s Response at 777 users 70.2s 72s 2.7s First error 405 Users 225 Users 884 Users

Quelle

CPU-Auslastung

Quelle

Speicherverbrauch

System Memory 01:00 02:00 Difference = Consumed Memory HTTP 3,357 MB 3,416 MB 59 MB HTTPS 3,500 MB 3,579 MB 79 MB SPDY 3,607 MB 3,631 MB 24 MB

Quelle

SPDY

Desktop:

✔ ✘ ✔ 11+*

*= nur IE11 auf Windows 8+

Can I Use

SPDY

Mobile:

3+ 33+ ✘ 24+ 11+? (WP8.1)

Can I Use

SPDY

Serverunterstützung

Seiten, die SPDY nutzen

  • Google
  • Facebook
  • WordPress.com

Zukunft

  • HTTP/3 mit weiteren Features wie z.B. DNS-Push
  • QUIC

Weiterführende Literatur

Vermischtes

(9)

requestAnimationFrame

Touch Delays abschalten

Chrome & Firefox:

<meta name="viewport" content="width=device-width">

Alle anderen Browser:

<meta name="viewport" content="width=device-width, user-scalable=no">

Siehe

ping-Attribut für Links

WebWorker (Anwendungsfall)

Event Capturing

HTML-Volumen durch HTML Imports reduzieren, Caching atomarisieren

HTML-Volumen durch Web Components reduzieren

JavaScript durch input:checked + x oder a:target ersetzen

Bildvolumen mit <img srcset/> und dem <picture>-Element reduzieren

Danke!

0