HTML5-Techniken zur Optimierung der Leistung auf Mobilgeräten

Wesley Hales
Wesley Hales

Einführung

Das Drehen zum Aktualisieren, ruckartige Seitenübergänge und regelmäßige Verzögerungen bei Tippereignissen sind nur einige der Probleme, die in den heutigen mobilen Webumgebungen auftreten. Entwickler versuchen, so nah wie möglich an die native Entwicklung heranzukommen, werden aber oft durch Hacks, Resets und starre Frameworks ausgebremst.

In diesem Artikel geht es um die Mindestanforderungen für die Entwicklung einer mobilen HTML5-Web-App. Dabei soll vor allem die Komplexität aufgedeckt werden, die in den heutigen mobilen Frameworks verborgen ist. Sie sehen einen minimalistischen Ansatz (mit Core-HTML5-APIs) und grundlegende Konzepte, die Ihnen helfen, Ihr eigenes Framework zu schreiben oder zu dem Framework beizutragen, das Sie derzeit verwenden.

Hardwarebeschleunigung

Normalerweise werden detaillierte 3D-Modelle oder CAD-Diagramme von GPUs verarbeitet. In diesem Fall sollen jedoch unsere einfachen Zeichnungen (Divs, Hintergründe, Text mit Schlagschatten, Bilder usw.) flüssig dargestellt und über die GPU animiert werden. Leider lagern die meisten Frontend-Entwickler diesen Animationsprozess an ein Drittanbieter-Framework aus, ohne sich um die Semantik zu kümmern. Sollten diese wichtigen CSS3-Funktionen maskiert werden? Hier sind einige Gründe, warum es wichtig ist, sich damit zu beschäftigen:

  1. Speicherzuweisung und Rechenaufwand: Wenn Sie jedes Element im DOM nur um der Hardwarebeschleunigung willen zusammenfügen, wird die nächste Person, die an Ihrem Code arbeitet, Sie vielleicht verfolgen und schwer verprügeln.

  2. Stromverbrauch: Wenn Hardware zum Einsatz kommt, wird natürlich auch der Akku belastet. Bei der Entwicklung für Mobilgeräte müssen Entwickler die vielen Einschränkungen von Geräten berücksichtigen, wenn sie mobile Web-Apps schreiben. Das wird noch wichtiger, wenn Browserhersteller den Zugriff auf immer mehr Gerätehardware ermöglichen.

  3. Konflikte: Beim Anwenden der Hardwarebeschleunigung auf Teile der Seite, die bereits beschleunigt wurden, traten Fehler auf. Es ist also sehr wichtig zu wissen, ob Sie sich überschneidende Beschleunigungen haben.

Damit die Nutzerinteraktion reibungslos und so nativ wie möglich abläuft, müssen wir den Browser für uns arbeiten lassen. Im Idealfall soll die CPU des Mobilgeräts die anfängliche Animation einrichten und die GPU nur für das Zusammensetzen verschiedener Ebenen während der Animation verantwortlich sein. Das ist die Funktion von „translate3d“, „scale3d“ und „translateZ“: Sie weisen den animierten Elementen eine eigene Ebene zu, sodass das Gerät alles zusammen reibungslos rendern kann. Weitere Informationen zu Accelerated Compositing und zur Funktionsweise von WebKit finden Sie in diesem Blogpost von Ariya Hidayat.

Seitenübergänge

Sehen wir uns drei der häufigsten Ansätze für Nutzerinteraktionen bei der Entwicklung einer mobilen Web-App an: Folien-, Flip- und Rotationseffekte.

Sie können diesen Code in Aktion unter http://slidfast.appspot.com/slide-flip-rotate.html sehen. Hinweis: Diese Demo wurde für ein Mobilgerät entwickelt. Starten Sie also einen Emulator, verwenden Sie Ihr Smartphone oder Tablet oder verkleinern Sie das Browserfenster auf ca. 1.024 Pixel oder weniger.

Zuerst sehen wir uns die Übergänge „Folie“, „Umblättern“ und „Drehen“ an und wie sie beschleunigt werden. Jede Animation besteht nur aus drei oder vier Zeilen CSS- und JavaScript-Code.

Fließend

Die häufigste der drei Übergangsmethoden, die gleitenden Seitenübergänge, ahmt das native Verhalten von mobilen Anwendungen nach. Die Folienübergänge werden aufgerufen, um einen neuen Inhaltsbereich in den Viewport zu bringen.

Für den Folien-Effekt deklarieren wir zuerst das Markup:

<div id="home-page" class="page">
  <h1>Home Page</h1>
</div>

<div id="products-page" class="page stage-right">
  <h1>Products Page</h1>
</div>

<div id="about-page" class="page stage-left">
  <h1>About Page</h1>
</div>

Wir haben das Konzept, Seiten links oder rechts zu platzieren. Das kann im Grunde jede Richtung sein, aber das ist am häufigsten.

Wir haben jetzt eine Animation mit Hardwarebeschleunigung, die nur wenige Zeilen CSS erfordert. Die eigentliche Animation erfolgt, wenn wir die Klassen der Div-Elemente auf der Seite tauschen.

.page {
  position: absolute;
  width: 100%;
  height: 100%;
  /*activate the GPU for compositing each page */
  -webkit-transform: translate3d(0, 0, 0);
}

translate3d(0,0,0) wird als „Silver Bullet“-Ansatz bezeichnet.

Wenn der Nutzer auf ein Navigationselement klickt, führen wir den folgenden JavaScript-Code aus, um die Klassen zu tauschen. Es werden keine Drittanbieter-Frameworks verwendet, sondern nur JavaScript. ;)

function getElement(id) {
  return document.getElementById(id);
}

function slideTo(id) {
  //1.) the page we are bringing into focus dictates how
  // the current page will exit. So let's see what classes
  // our incoming page is using. We know it will have stage[right|left|etc...]
  var classes = getElement(id).className.split(' ');

  //2.) decide if the incoming page is assigned to right or left
  // (-1 if no match)
  var stageType = classes.indexOf('stage-left');

  //3.) on initial page load focusPage is null, so we need
  // to set the default page which we're currently seeing.
  if (FOCUS_PAGE == null) {
    // use home page
    FOCUS_PAGE = getElement('home-page');
  }

  //4.) decide how this focused page should exit.
  if (stageType > 0) {
    FOCUS_PAGE.className = 'page transition stage-right';
  } else {
    FOCUS_PAGE.className = 'page transition stage-left';
  }

  //5. refresh/set the global variable
  FOCUS_PAGE = getElement(id);

  //6. Bring in the new page.
  FOCUS_PAGE.className = 'page transition stage-center';
}

stage-left oder stage-right wird zu stage-center und die Seite wird in den mittleren Anzeigebereich verschoben. Wir verlassen uns vollständig auf CSS3, um die Hauptarbeit zu erledigen.

.stage-left {
  left: -480px;
}

.stage-right {
  left: 480px;
}

.stage-center {
  top: 0;
  left: 0;
}

Sehen wir uns als Nächstes das CSS an, das für die Erkennung und Ausrichtung von Mobilgeräten zuständig ist. Wir konnten jedes Gerät und jede Auflösung ansprechen (siehe Auflösung der Media-Query). In dieser Demo habe ich nur einige einfache Beispiele verwendet, um die meisten Hoch- und Querformatansichten auf Mobilgeräten abzudecken. Dies ist auch nützlich, um die Hardwarebeschleunigung pro Gerät anzuwenden. Da die Desktopversion von WebKit beispielsweise alle transformierten Elemente beschleunigt (unabhängig davon, ob es sich um 2D- oder 3D-Elemente handelt), ist es sinnvoll, eine Media-Query zu erstellen und die Beschleunigung auf dieser Ebene auszuschließen. Hinweis: Hardwarebeschleunigung bietet unter Android Froyo 2.2+ keine Geschwindigkeitsverbesserung. Die gesamte Komposition erfolgt in der Software.

/* iOS/android phone landscape screen width*/
@media screen and (max-device-width: 480px) and (orientation:landscape) {
  .stage-left {
    left: -480px;
  }

  .stage-right {
    left: 480px;
  }

  .page {
    width: 480px;
  }
}

Spiegeln

Auf Mobilgeräten wird das Umblättern als Wegwischen der Seite bezeichnet. Hier verwenden wir einfaches JavaScript, um dieses Ereignis auf iOS- und Android-Geräten (WebKit-basiert) zu verarbeiten.

http://slidfast.appspot.com/slide-flip-rotate.html

Wenn Sie mit Touch-Ereignissen und Übergängen arbeiten, möchten Sie als Erstes die aktuelle Position des Elements ermitteln. Weitere Informationen zu WebKitCSSMatrix finden Sie in diesem Dokument.

function pageMove(event) {
  // get position after transform
  var curTransform = new WebKitCSSMatrix(window.getComputedStyle(page).webkitTransform);
  var pagePosition = curTransform.m41;
}

Da wir für das Umblättern der Seite eine CSS3-Ease-Out-Übergangseinstellung verwenden, funktioniert das übliche element.offsetLeft nicht.

Als Nächstes möchten wir herausfinden, in welche Richtung der Nutzer wischt, und einen Schwellenwert für ein Ereignis (Seitennavigation) festlegen.

if (pagePosition >= 0) {
 //moving current page to the right
 //so means we're flipping backwards
   if ((pagePosition > pageFlipThreshold) || (swipeTime < swipeThreshold)) {
     //user wants to go backward
     slideDirection = 'right';
   } else {
     slideDirection = null;
   }
} else {
  //current page is sliding to the left
  if ((swipeTime < swipeThreshold) || (pagePosition < pageFlipThreshold)) {
    //user wants to go forward
    slideDirection = 'left';
  } else {
    slideDirection = null;
  }
}

Außerdem sehen Sie, dass wir die swipeTime auch in Millisekunden messen. So kann das Navigationsereignis ausgelöst werden, wenn der Nutzer schnell über den Bildschirm wischt, um eine Seite umzublättern.

Um die Seite zu positionieren und die Animationen natürlich aussehen zu lassen, während ein Finger den Bildschirm berührt, verwenden wir nach jedem Ereignis, das ausgelöst wird, CSS3-Übergänge.

function positionPage(end) {
  page.style.webkitTransform = 'translate3d('+ currentPos + 'px, 0, 0)';
  if (end) {
    page.style.WebkitTransition = 'all .4s ease-out';
    //page.style.WebkitTransition = 'all .4s cubic-bezier(0,.58,.58,1)'
  } else {
    page.style.WebkitTransition = 'all .2s ease-out';
  }
  page.style.WebkitUserSelect = 'none';
}

Ich habe versucht, mit „cubic-bezier“ den Übergängen das beste native Gefühl zu verleihen, aber „ease-out“ hat es getan.

Damit die Navigation erfolgt, müssen wir schließlich die zuvor definierten slideTo()-Methoden aufrufen, die wir in der letzten Demo verwendet haben.

track.ontouchend = function(event) {
  pageMove(event);
  if (slideDirection == 'left') {
    slideTo('products-page');
  } else if (slideDirection == 'right') {
    slideTo('home-page');
  }
}

Drehen

Als Nächstes sehen wir uns die in dieser Demo verwendete Rotationsanimation an. Sie können die Seite, die Sie gerade sehen, jederzeit um 180° drehen, um die Rückseite zu sehen. Tippen Sie dazu auf die Menüoption „Kontakt“. Auch hier sind nur wenige Zeilen CSS und etwas JavaScript erforderlich, um eine Übergangsklasse onclick zuzuweisen. HINWEIS: Die Drehungsübergänge werden auf den meisten Android-Versionen nicht richtig gerendert, da es keine 3D-CSS-Transformationsfunktionen gibt. Leider wird die Seite unter Android nicht einfach umgedreht, sondern sie dreht sich wie ein Rad. Wir empfehlen, diese Übergänge nur sparsam zu verwenden, bis die Unterstützung verbessert wird.

Das Markup (grundlegendes Konzept von Vorder- und Rückseite):

<div id="front" class="normal">
...
</div>
<div id="back" class="flipped">
    <div id="contact-page" class="page">
        <h1>Contact Page</h1>
    </div>
</div>

Das JavaScript:

function flip(id) {
  // get a handle on the flippable region
  var front = getElement('front');
  var back = getElement('back');

  // again, just a simple way to see what the state is
  var classes = front.className.split(' ');
  var flipped = classes.indexOf('flipped');

  if (flipped >= 0) {
    // already flipped, so return to original
    front.className = 'normal';
    back.className = 'flipped';
    FLIPPED = false;
  } else {
    // do the flip
    front.className = 'flipped';
    back.className = 'normal';
    FLIPPED = true;
  }
}

Das CSS:

/*----------------------------flip transition */
#back,
#front {
  position: absolute;
  width: 100%;
  height: 100%;
  -webkit-backface-visibility: hidden;
  -webkit-transition-duration: .5s;
  -webkit-transform-style: preserve-3d;
}

.normal {
  -webkit-transform: rotateY(0deg);
}

.flipped {
  -webkit-user-select: element;
  -webkit-transform: rotateY(180deg);
}

Fehlerbehebung bei der Hardwarebeschleunigung

Nachdem wir uns die grundlegenden Übergänge angesehen haben, wollen wir uns nun ansehen, wie sie funktionieren und zusammengesetzt werden.

Damit diese magische Debugging-Sitzung stattfinden kann, starten wir ein paar Browser und Ihre bevorzugte IDE. Starten Sie Safari zuerst über die Befehlszeile, um einige Umgebungsvariablen für das Debugging zu verwenden. Ich verwende einen Mac, daher können sich die Befehle je nach Betriebssystem unterscheiden. Öffnen Sie das Terminal und geben Sie Folgendes ein:

  • $> export CA_COLOR_OPAQUE=1
  • $> export CA_LOG_MEMORY_USAGE=1
  • $> /Applications/Safari.app/Contents/MacOS/Safari

Dadurch wird Safari mit einigen Debugging-Helfern gestartet. CA_COLOR_OPAQUE zeigt uns, welche Elemente tatsächlich zusammengesetzt oder beschleunigt werden. CA_LOG_MEMORY_USAGE zeigt uns, wie viel Arbeitsspeicher wir verwenden, wenn wir unsere Zeichenvorgänge an den Backing Store senden. So erfahren Sie genau, wie stark das Mobilgerät beansprucht wird, und erhalten möglicherweise Hinweise darauf, wie der Akku des Zielgeräts durch die GPU-Nutzung entladen wird.

Jetzt starten wir Chrome, um Informationen zu den Bildern pro Sekunde (FPS) zu sehen:

  1. Öffnen Sie den Google Chrome-Webbrowser.
  2. Geben Sie in die URL-Leiste about:flags ein.
  3. Scrollen Sie einige Elemente nach unten und klicken Sie für „FPS Counter“ (FPS-Zähler) auf „Enable“ (Aktivieren).

Wenn Sie diese Seite in Ihrer optimierten Chrome-Version aufrufen, sehen Sie oben links den roten FPS-Zähler.

Chrome FPS

So erkennen Sie, ob die Hardwarebeschleunigung aktiviert ist. Außerdem können wir so sehen, wie die Animation ausgeführt wird und ob es Lecks gibt (kontinuierlich laufende Animationen, die beendet werden sollten).

Eine weitere Möglichkeit, die Hardwarebeschleunigung zu visualisieren, besteht darin, dieselbe Seite in Safari zu öffnen (mit den oben genannten Umgebungsvariablen). Jedes beschleunigte DOM-Element hat einen roten Farbton. So können wir genau sehen, was nach Ebene zusammengesetzt wird. Die weiße Navigation ist nicht rot, da sie nicht beschleunigt wird.

Zusammengesetzter Kontakt

Eine ähnliche Einstellung für Chrome ist auch in about:flags unter „Composited render layer borders“ verfügbar.

Eine weitere gute Möglichkeit, die zusammengesetzten Ebenen zu sehen, ist die WebKit-Demo mit fallenden Blättern, während dieser Mod angewendet wird.

Zusammengesetzte Blätter

Um die Leistung der Grafikhardware unserer Anwendung wirklich zu verstehen, sehen wir uns an, wie der Speicher genutzt wird. Hier sehen wir, dass wir 1,38 MB an Zeichenanweisungen in die CoreAnimation-Puffer unter Mac OS übertragen. Die Core Animation-Arbeitsspeicherpuffer werden zwischen OpenGL ES und der GPU gemeinsam genutzt, um die endgültigen Pixel zu erstellen, die auf dem Bildschirm angezeigt werden.

Coreanimation 1

Wenn wir das Browserfenster einfach vergrößern oder maximieren, sehen wir auch, dass sich der Speicher erweitert.

Coreanimation 2

So erhalten Sie einen Eindruck davon, wie viel Speicherplatz auf Ihrem Mobilgerät belegt wird, wenn Sie die Größe des Browsers anpassen. Wenn Sie für iPhone-Umgebungen debuggen oder testen, ändern Sie die Größe auf 480 × 320 Pixel. Wir wissen jetzt genau, wie die Hardwarebeschleunigung funktioniert und was zum Debuggen erforderlich ist. Es ist eine Sache, darüber zu lesen, aber die GPU-Arbeitsspeicherpuffer in Aktion zu sehen, macht die Dinge wirklich anschaulich.

Hinter den Kulissen: Abrufen und Zwischenspeichern

Jetzt ist es an der Zeit, das Caching von Seiten und Ressourcen auf die nächste Stufe zu heben. Ähnlich wie bei jQuery Mobile und ähnlichen Frameworks werden wir unsere Seiten mit gleichzeitigen AJAX-Aufrufen vorab abrufen und im Cache speichern.

Sehen wir uns einige der wichtigsten Probleme im mobilen Web an und warum wir sie beheben müssen:

  • Abrufen: Durch das Vorabrufen unserer Seiten können Nutzer die App offline verwenden. Außerdem müssen sie nicht zwischen Navigationsaktionen warten. Wir möchten natürlich nicht, dass die Bandbreite des Geräts eingeschränkt wird, wenn es online geht. Daher sollten wir diese Funktion nur sparsam einsetzen.
  • Caching: Als Nächstes benötigen wir einen gleichzeitigen oder asynchronen Ansatz zum Abrufen und Cachen dieser Seiten. Außerdem müssen wir localStorage verwenden, da es auf vielen Geräten unterstützt wird. Leider ist es nicht asynchron.
  • AJAX und Parsen der Antwort: Die Verwendung von innerHTML(), um die AJAX-Antwort in das DOM einzufügen, ist gefährlich (und unzuverlässig?). Stattdessen verwenden wir einen zuverlässigen Mechanismus zum Einfügen von AJAX-Antworten und zur Verarbeitung gleichzeitiger Aufrufe. Außerdem nutzen wir einige neue Funktionen von HTML5, um die xhr.responseText zu parsen.

Wir bauen auf dem Code aus der Demo für das Einblenden, Umblättern und Drehen auf und fügen zuerst einige sekundäre Seiten hinzu und verlinken sie. Die Links werden dann analysiert und Übergänge werden spontan erstellt.

iPhone Home

Demo für „Abrufen und im Cache speichern“ ansehen

Wie Sie sehen, verwenden wir hier semantisches Markup. Nur ein Link zu einer anderen Seite. Die untergeordnete Seite hat dieselbe Knoten-/Klassenstruktur wie die übergeordnete Seite. Wir könnten noch einen Schritt weiter gehen und das data-*-Attribut für „page“-Knoten usw. verwenden. Hier ist die Detailseite (untergeordnetes Element) in einer separaten HTML-Datei (/demo2/home-detail.html) enthalten, die beim Laden der App geladen, im Cache gespeichert und für den Übergang eingerichtet wird.

<div id="home-page" class="page">
  <h1>Home Page</h1>
  <a href="demo2/home-detail.html" class="fetch">Find out more about the home page!</a>
</div>

Sehen wir uns jetzt den JavaScript-Code an. Der Einfachheit halber lasse ich alle Hilfsfunktionen oder Optimierungen im Code weg. Wir durchlaufen hier lediglich ein angegebenes Array von DOM-Knoten, um Links zum Abrufen und Zwischenspeichern zu finden. Hinweis: In dieser Demo wird die Methode fetchAndCache() beim Laden der Seite aufgerufen. Wir überarbeiten sie im nächsten Abschnitt, wenn wir die Netzwerkverbindung erkennen und festlegen, wann sie aufgerufen werden soll.

var fetchAndCache = function() {
  // iterate through all nodes in this DOM to find all mobile pages we care about
  var pages = document.getElementsByClassName('page');

  for (var i = 0; i < pages.length; i++) {
    // find all links
    var pageLinks = pages[i].getElementsByTagName('a');

    for (var j = 0; j < pageLinks.length; j++) {
      var link = pageLinks[j];

      if (link.hasAttribute('href') &amp;&amp;
      //'#' in the href tells us that this page is already loaded in the DOM - and
      // that it links to a mobile transition/page
         !(/[\#]/g).test(link.href) &amp;&amp;
        //check for an explicit class name setting to fetch this link
        (link.className.indexOf('fetch') >= 0))  {
         //fetch each url concurrently
         var ai = new ajax(link,function(text,url){
              //insert the new mobile page into the DOM
             insertPages(text,url);
         });
         ai.doGet();
      }
    }
  }
};

Wir sorgen für eine ordnungsgemäße asynchrone Nachbearbeitung durch die Verwendung des „AJAX“-Objekts. Eine ausführlichere Erklärung zur Verwendung von „localStorage“ in einem AJAX-Aufruf finden Sie unter Working Off the Grid with HTML5 Offline. In diesem Beispiel sehen Sie die grundlegende Verwendung von Caching bei jeder Anfrage und die Bereitstellung der im Cache gespeicherten Objekte, wenn der Server eine andere als eine erfolgreiche (200) Antwort zurückgibt.

function processRequest () {
  if (req.readyState == 4) {
    if (req.status == 200) {
      if (supports_local_storage()) {
        localStorage[url] = req.responseText;
      }
      if (callback) callback(req.responseText,url);
    } else {
      // There is an error of some kind, use our cached copy (if available).
      if (!!localStorage[url]) {
        // We have some data cached, return that to the callback.
        callback(localStorage[url],url);
        return;
      }
    }
  }
}

Da localStorage UTF-16 für die Zeichencodierung verwendet, wird jedes einzelne Byte als 2 Byte gespeichert.Das Speicherlimit wird dadurch von 5 MB auf insgesamt 2, 6 MB reduziert. Der Grund für das Abrufen und Zwischenspeichern dieser Seiten/dieses Markups außerhalb des Anwendungs-Cache-Bereichs wird im nächsten Abschnitt erläutert.

Dank der jüngsten Fortschritte beim iframe-Element mit HTML5 haben wir jetzt eine einfache und effektive Möglichkeit, die responseText zu parsen, die wir von unserem AJAX-Aufruf zurückerhalten. Es gibt viele 3.000 Zeilen lange JavaScript-Parser und reguläre Ausdrücke, mit denen Script-Tags usw. entfernt werden. Warum nicht den Browser das tun lassen, was er am besten kann? In diesem Beispiel schreiben wir den responseText in ein temporäres, ausgeblendetes iFrame. Wir verwenden das HTML5-Attribut „sandbox“, das Skripts deaktiviert und viele Sicherheitsfunktionen bietet…

Aus der Spezifikation: Wenn das Attribut „sandbox“ angegeben wird, werden für alle Inhalte, die vom iFrame gehostet werden, zusätzliche Einschränkungen aktiviert. Der Wert muss eine ungeordnete Menge eindeutiger, durch Leerzeichen getrennter Tokens sein, bei denen die Groß- und Kleinschreibung keine Rolle spielt. Zulässige Werte sind allow-forms, allow-same-origin, allow-scripts und allow-top-navigation. Wenn das Attribut festgelegt ist, werden die Inhalte als von einem eindeutigen Ursprung behandelt, Formulare und Scripts werden deaktiviert, Links können nicht auf andere Browserkontexte verweisen und Plug-ins werden deaktiviert.

var insertPages = function(text, originalLink) {
  var frame = getFrame();
  //write the ajax response text to the frame and let
  //the browser do the work
  frame.write(text);

  //now we have a DOM to work with
  var incomingPages = frame.getElementsByClassName('page');

  var pageCount = incomingPages.length;
  for (var i = 0; i < pageCount; i++) {
    //the new page will always be at index 0 because
    //the last one just got popped off the stack with appendChild (below)
    var newPage = incomingPages[0];

    //stage the new pages to the left by default
    newPage.className = 'page stage-left';

    //find out where to insert
    var location = newPage.parentNode.id == 'back' ? 'back' : 'front';

    try {
      // mobile safari will not allow nodes to be transferred from one DOM to another so
      // we must use adoptNode()
      document.getElementById(location).appendChild(document.adoptNode(newPage));
    } catch(e) {
      // todo graceful degradation?
    }
  }
};

Safari weigert sich korrekt, einen Knoten implizit von einem Dokument in ein anderes zu verschieben. Es wird ein Fehler ausgegeben, wenn der neue untergeordnete Knoten in einem anderen Dokument erstellt wurde. Hier verwenden wir also adoptNode und alles ist in Ordnung.

Warum also iFrame? Warum nicht einfach innerHTML verwenden? Obwohl innerHTML jetzt Teil der HTML5-Spezifikation ist, ist es gefährlich, die Antwort eines Servers (böse oder gut) in einen ungeprüften Bereich einzufügen. Beim Schreiben dieses Artikels habe ich niemanden gefunden, der etwas anderes als innerHTML verwendet. Ich weiß, dass jQuery es im Kern verwendet und nur bei einer Ausnahme auf „append“ zurückgreift. Und JQuery Mobile verwendet es auch. Ich habe jedoch keine umfangreichen Tests in Bezug auf innerHTML durchgeführt, das zufällig nicht mehr funktioniert. Es wäre sehr interessant zu sehen, welche Plattformen davon betroffen sind. Es wäre auch interessant zu sehen, welcher Ansatz leistungsfähiger ist. Ich habe dazu schon von beiden Seiten Behauptungen gehört.

Erkennung, Verarbeitung und Profilerstellung von Netzwerktypen

Da wir unsere Web-App jetzt puffern (oder vorab im Cache speichern) können, müssen wir die entsprechenden Funktionen zur Erkennung von Verbindungen bereitstellen, damit unsere App intelligenter wird. Hier ist die Entwicklung mobiler Apps besonders anfällig für Online-/Offlinemodi und die Verbindungsgeschwindigkeit. Geben Sie The Network Information API ein. Jedes Mal, wenn ich diese Funktion in einer Präsentation zeige, meldet sich jemand im Publikum und fragt: „Wofür kann ich das verwenden?“ Hier ist also eine mögliche Vorgehensweise, um eine extrem intelligente mobile Web-App einzurichten.

Zuerst ein langweiliges, aber wichtiges Szenario: Wenn Sie auf einem Mobilgerät in einem Hochgeschwindigkeitszug im Web surfen, kann es durchaus sein, dass die Netzwerkverbindung an verschiedenen Stellen unterbrochen wird. Außerdem werden in verschiedenen Regionen möglicherweise unterschiedliche Übertragungsgeschwindigkeiten unterstützt (z. B. HSPA oder 3G sind möglicherweise in einigen städtischen Gebieten verfügbar, in abgelegenen Gebieten werden jedoch möglicherweise viel langsamere 2G-Technologien unterstützt. Der folgende Code deckt die meisten Verbindungsszenarien ab.

Der folgende Code enthält:

  • Offlinezugriff über applicationCache
  • Erkennt, ob ein Ort mit einem Lesezeichen versehen und offline verfügbar ist.
  • Erkennt, wenn von offline zu online und umgekehrt gewechselt wird.
  • Erkennt langsame Verbindungen und ruft Inhalte basierend auf dem Netzwerktyp ab.

Auch für diese Funktionen ist nur sehr wenig Code erforderlich. Zuerst erkennen wir unsere Ereignisse und Ladeszenarien:

window.addEventListener('load', function(e) {
 if (navigator.onLine) {
  // new page load
  processOnline();
 } else {
   // the app is probably already cached and (maybe) bookmarked...
   processOffline();
 }
}, false);

window.addEventListener("offline", function(e) {
  // we just lost our connection and entered offline mode, disable eternal link
  processOffline(e.type);
}, false);

window.addEventListener("online", function(e) {
  // just came back online, enable links
  processOnline(e.type);
}, false);

In den oben genannten EventListeners müssen wir unserem Code mitteilen, ob er von einem Ereignis oder einer tatsächlichen Seitenanfrage oder ‑aktualisierung aufgerufen wird. Der Hauptgrund dafür ist, dass das onload-Ereignis nicht ausgelöst wird, wenn zwischen dem Online- und dem Offlinemodus gewechselt wird.

Als Nächstes folgt eine einfache Prüfung auf ein ononline- oder onload-Ereignis. Dieser Code setzt deaktivierte Links zurück, wenn von offline zu online gewechselt wird. Wenn diese App jedoch komplexer wäre, könnten Sie Logik einfügen, die das Abrufen von Inhalten fortsetzt oder die Benutzeroberfläche für zeitweilige Verbindungen verarbeitet.

function processOnline(eventType) {

  setupApp();
  checkAppCache();

  // reset our once disabled offline links
  if (eventType) {
    for (var i = 0; i < disabledLinks.length; i++) {
      disabledLinks[i].onclick = null;
    }
  }
}

Dasselbe gilt für processOffline(). Hier können Sie Ihre App für den Offlinemodus anpassen und versuchen, alle Transaktionen wiederherzustellen, die im Hintergrund stattgefunden haben. Der folgende Code sucht alle unsere externen Links und deaktiviert sie. So werden Nutzer für IMMER in unserer Offline-App gefangen – muhahaha!

function processOffline() {
  setupApp();

  // disable external links until we come back - setting the bounds of app
  disabledLinks = getUnconvertedLinks(document);

  // helper for onlcick below
  var onclickHelper = function(e) {
    return function(f) {
      alert('This app is currently offline and cannot access the hotness');return false;
    }
  };

  for (var i = 0; i < disabledLinks.length; i++) {
    if (disabledLinks[i].onclick == null) {
      //alert user we're not online
      disabledLinks[i].onclick = onclickHelper(disabledLinks[i].href);

    }
  }
}

Okay, kommen wir zum Wesentlichen. Jetzt, da unsere App weiß, in welchem Verbindungsstatus sie sich befindet, können wir auch den Verbindungstyp prüfen, wenn sie online ist, und die Einstellungen entsprechend anpassen. Ich habe in den Kommentaren für jede Verbindung die typischen Download- und Latenzwerte von nordamerikanischen Anbietern aufgeführt.

function setupApp(){
  // create a custom object if navigator.connection isn't available
  var connection = navigator.connection || {'type':'0'};
  if (connection.type == 2 || connection.type == 1) {
      //wifi/ethernet
      //Coffee Wifi latency: ~75ms-200ms
      //Home Wifi latency: ~25-35ms
      //Coffee Wifi DL speed: ~550kbps-650kbps
      //Home Wifi DL speed: ~1000kbps-2000kbps
      fetchAndCache(true);
  } else if (connection.type == 3) {
  //edge
      //ATT Edge latency: ~400-600ms
      //ATT Edge DL speed: ~2-10kbps
      fetchAndCache(false);
  } else if (connection.type == 2) {
      //3g
      //ATT 3G latency: ~400ms
      //Verizon 3G latency: ~150-250ms
      //ATT 3G DL speed: ~60-100kbps
      //Verizon 3G DL speed: ~20-70kbps
      fetchAndCache(false);
  } else {
  //unknown
      fetchAndCache(true);
  }
}

Wir könnten zahlreiche Anpassungen an unserem fetchAndCache-Prozess vornehmen. Ich habe hier aber nur festgelegt, ob die Ressourcen für eine bestimmte Verbindung asynchron (true) oder synchron (false) abgerufen werden sollen.

Zeitachse für Edge-Anfragen (synchron)

Edge-Synchronisierung

Zeitachse für WLAN-Anfragen (asynchron)

WIFI Async

So kann die Nutzererfahrung zumindest in gewissem Maße an langsame oder schnelle Verbindungen angepasst werden. Dies ist keineswegs eine Allround-Lösung. Außerdem sollte ein Lademodal angezeigt werden, wenn auf einen Link geklickt wird (bei langsamen Verbindungen), während die App die Seite des Links möglicherweise noch im Hintergrund abruft. Das Ziel ist, die Latenz zu verringern und gleichzeitig die volle Leistungsfähigkeit der Verbindung des Nutzers mit den neuesten HTML5-Funktionen zu nutzen. Demo zur Netzwerkerkennung ansehen

Fazit

Die Entwicklung mobiler HTML5-Apps steht noch ganz am Anfang. Sie sehen nun die sehr einfachen und grundlegenden Grundlagen eines mobilen Frameworks, das ausschließlich auf HTML5 und den zugehörigen Technologien basiert. Ich halte es für wichtig, dass Entwickler diese Funktionen in ihrer ursprünglichen Form nutzen und nicht durch einen Wrapper maskieren.