Techniques HTML5 permettant d'optimiser les performances sur mobile

Introduction

Les actualisations en boucle, les transitions de page saccadées et les retards périodiques dans les événements d'appui ne sont que quelques-uns des problèmes rencontrés dans les environnements Web mobiles actuels. Les développeurs essaient de se rapprocher le plus possible du natif, mais sont souvent déroutés par des hacks, des réinitialisations et des frameworks rigides.

Dans cet article, nous allons aborder le strict minimum nécessaire pour créer une application Web HTML5 mobile. L'objectif principal est de dévoiler les complexités cachées que les frameworks mobiles actuels tentent de masquer. Vous découvrirez une approche minimaliste (utilisant les API HTML5 de base) et les principes fondamentaux qui vous permettront d'écrire votre propre framework ou de contribuer à celui que vous utilisez actuellement.

Accélération matérielle

En temps normal, les GPU gèrent la modélisation 3D détaillée ou les diagrammes CAO. Dans ce cas, nous voulons que nos dessins primitifs (divs, arrière-plans, texte avec ombres portées, images, etc.) apparaissent de manière fluide et s'animent de manière fluide via le GPU. Malheureusement, la plupart des développeurs front-end délèguent ce processus d'animation à un framework tiers sans se soucier de la sémantique. Ces fonctionnalités CSS3 de base doivent-elles être masquées ? Voici quelques raisons pour lesquelles il est important de s'intéresser à ces éléments :

  1. Allocation de mémoire et charge de calcul : si vous composez chaque élément du DOM uniquement pour l'accélération matérielle, la prochaine personne qui travaillera sur votre code risque de vous rattraper et de vous battre à mort.

  2. Consommation d'énergie : lorsque le matériel se met en marche, la batterie se met également en marche. Lorsqu'ils développent pour mobile, les développeurs sont obligés de prendre en compte le large éventail de contraintes liées aux appareils lorsqu'ils écrivent des applications Web mobiles. Cela sera encore plus fréquent à mesure que les fabricants de navigateurs commenceront à permettre l'accès à de plus en plus de matériel d'appareils.

  3. Conflits : j'ai rencontré des problèmes de fonctionnement lorsque j'ai appliqué l'accélération matérielle à des parties de la page qui étaient déjà accélérées. Il est donc très important de savoir si vous avez une accélération qui se chevauche.

Pour que l'interaction utilisateur soit fluide et aussi proche que possible de l'expérience native, nous devons faire en sorte que le navigateur travaille pour nous. Idéalement, nous souhaitons que le processeur de l'appareil mobile configure l'animation initiale, puis que le GPU soit responsable uniquement de la composition des différentes couches pendant le processus d'animation. C'est ce que font translate3d, scale3d et translateZ : ils donnent aux éléments animés leur propre calque, ce qui permet à l'appareil de tout afficher ensemble de manière fluide. Pour en savoir plus sur la composition accélérée et le fonctionnement de WebKit, Ariya Hidayat a publié de nombreuses informations utiles sur son blog.

Transitions de page

Examinons trois des approches d'interaction utilisateur les plus courantes lors du développement d'une application Web mobile : les effets de glissement, d'inversion et de rotation.

Vous pouvez voir ce code en action ici : http://slidfast.appspot.com/slide-flip-rotate.html (Remarque : Cette démo est conçue pour un appareil mobile. Lancez donc un émulateur, utilisez votre téléphone ou votre tablette, ou réduisez la taille de la fenêtre de votre navigateur à environ 1 024 pixels ou moins).

Nous allons commencer par décortiquer les transitions de diapos, de retournement et de rotation, et la façon dont elles sont accélérées. Remarquez que chaque animation ne nécessite que trois ou quatre lignes de code CSS et JavaScript.

Glissant

La plus courante des trois approches de transition, la transition de page coulissante imite l'aspect natif des applications mobiles. La transition de diapositive est appelée pour afficher une nouvelle zone de contenu dans la fenêtre d'affichage.

Pour l'effet de glissement, nous déclarons d'abord notre balisage :

<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>

Remarquez que nous avons ce concept de pages de préparation à gauche ou à droite. Il peut s'agir de n'importe quelle direction, mais c'est la plus courante.

Nous avons maintenant une animation et une accélération matérielle avec seulement quelques lignes de CSS. L'animation proprement dite se produit lorsque nous échangeons des classes sur les éléments div de la page.

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

translate3d(0,0,0) est appelée approche "solution miracle".

Lorsque l'utilisateur clique sur un élément de navigation, nous exécutons le code JavaScript suivant pour échanger les classes. Aucun framework tiers n'est utilisé. Il s'agit de JavaScript pur et simple. ;)

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 ou stage-right devient stage-center et force la page à glisser dans la fenêtre d'affichage centrale. Nous dépendons entièrement de CSS3 pour faire le gros du travail.

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

.stage-right {
  left: 480px;
}

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

Examinons maintenant le CSS qui gère la détection et l'orientation des appareils mobiles. Nous pourrions aborder chaque appareil et chaque résolution (voir résolution des requêtes média). Dans cette démo, je n'ai utilisé que quelques exemples simples pour couvrir la plupart des vues portrait et paysage sur les appareils mobiles. Cela permet également d'appliquer l'accélération matérielle par appareil. Par exemple, étant donné que la version pour ordinateur de WebKit accélère tous les éléments transformés (qu'ils soient en 2D ou en 3D), il est judicieux de créer une requête média et d'exclure l'accélération à ce niveau. Notez que les astuces d'accélération matérielle n'améliorent pas la vitesse sous Android Froyo 2.2 et versions ultérieures. Toute la composition est effectuée dans le logiciel.

/* 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;
  }
}

Retournement

Sur les appareils mobiles, le terme "tourner la page" désigne le fait de balayer la page pour la faire disparaître. Ici, nous utilisons un code JavaScript simple pour gérer cet événement sur les appareils iOS et Android (basés sur WebKit).

Découvrez-le en action : http://slidfast.appspot.com/slide-flip-rotate.html.

Lorsque vous traitez des événements tactiles et des transitions, la première chose à faire est de déterminer la position actuelle de l'élément. Pour en savoir plus sur WebKitCSSMatrix, consultez cette documentation.

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

Étant donné que nous utilisons une transition CSS3 ease-out pour le retournement de page, le element.offsetLeft habituel ne fonctionnera pas.

Nous devons ensuite déterminer dans quelle direction l'utilisateur fait glisser son doigt et définir un seuil pour qu'un événement (navigation sur la page) ait lieu.

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;
  }
}

Vous remarquerez également que nous mesurons le swipeTime en millisecondes. Cela permet à l'événement de navigation de se déclencher si l'utilisateur balaye rapidement l'écran pour tourner une page.

Pour positionner la page et donner aux animations un aspect natif lorsqu'un doigt touche l'écran, nous utilisons des transitions CSS3 après chaque déclenchement d'événement.

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';
}

J'ai essayé de jouer avec cubic-bezier pour donner la meilleure sensation native aux transitions, mais ease-out a fait l'affaire.

Enfin, pour que la navigation ait lieu, nous devons appeler les méthodes slideTo() que nous avons définies précédemment et que nous avons utilisées dans la dernière démo.

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

Rotation

Examinons ensuite l'animation de rotation utilisée dans cette démo. À tout moment, vous pouvez faire pivoter la page que vous consultez actuellement de 180 degrés pour afficher le verso en appuyant sur l'option de menu "Contact". Là encore, quelques lignes de CSS et de JavaScript suffisent pour attribuer une classe de transition onclick. REMARQUE : La transition de rotation ne s'affiche pas correctement sur la plupart des versions d'Android, car elle ne dispose pas de fonctionnalités de transformation CSS 3D. Malheureusement, au lieu d'ignorer l'inversion, Android fait "rouler" la page en la faisant pivoter au lieu de l'inverser. Nous vous recommandons d'utiliser cette transition avec parcimonie jusqu'à ce que la prise en charge s'améliore.

Voici le balisage (concept de base pour l'avant et l'arrière) :

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

Le code 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;
  }
}

Le 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);
}

Déboguer l'accélération matérielle

Maintenant que nous avons abordé les transitions de base, examinons leur fonctionnement et leur composition.

Pour que cette session de débogage magique ait lieu, lançons quelques navigateurs et l'IDE de votre choix. Commencez par démarrer Safari à partir de la ligne de commande pour utiliser certaines variables d'environnement de débogage. J'utilise un Mac. Les commandes peuvent donc varier en fonction de votre système d'exploitation. Ouvrez le terminal et saisissez la commande suivante :

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

Safari démarre alors avec quelques outils de débogage. CA_COLOR_OPAQUE nous indique quels éléments sont réellement composés ou accélérés. CA_LOG_MEMORY_USAGE nous indique la quantité de mémoire que nous utilisons lorsque nous envoyons nos opérations de dessin au backing store. Cela vous indique exactement la charge que vous imposez à l'appareil mobile et peut vous donner des indications sur la façon dont votre utilisation du GPU peut décharger la batterie de l'appareil cible.

Maintenant, lançons Chrome pour afficher des informations intéressantes sur les images par seconde (FPS) :

  1. Ouvrez le navigateur Web Google Chrome.
  2. Dans la barre d'adresse, saisissez about:flags.
  3. Faites défiler la page de quelques éléments, puis cliquez sur "Activer" pour le compteur de FPS.

Si vous consultez cette page dans votre version améliorée de Chrome, le compteur de FPS rouge s'affiche en haut à gauche.

FPS Chrome

C'est ainsi que nous savons que l'accélération matérielle est activée. Il nous donne également une idée du fonctionnement de l'animation et de la présence éventuelle de fuites (animations en cours d'exécution qui devraient être arrêtées).

Une autre façon de visualiser l'accélération matérielle est d'ouvrir la même page dans Safari (avec les variables d'environnement mentionnées ci-dessus). Chaque élément DOM accéléré est teinté en rouge. Cela nous montre exactement ce qui est composé par calque. Notez que la navigation blanche n'est pas rouge, car elle n'est pas accélérée.

Contact composite

Un paramètre similaire pour Chrome est également disponible dans about:flags "Bordures des calques de rendu composite".

Un autre excellent moyen de voir les calques composites consiste à afficher la démonstration WebKit de feuilles mortes lorsque ce mod est appliqué.

Feuilles composées

Enfin, pour bien comprendre les performances du matériel graphique de notre application, examinons la consommation de mémoire. Ici, nous voyons que nous envoyons 1,38 Mo d'instructions de dessin aux tampons CoreAnimation sur Mac OS. Les tampons de mémoire Core Animation sont partagés entre OpenGL ES et le GPU pour créer les pixels finaux que vous voyez à l'écran.

Coreanimation 1

Lorsque nous redimensionnons ou agrandissons simplement la fenêtre du navigateur, nous constatons également que la mémoire se développe.

Coreanimation 2

Cela vous donne une idée de la façon dont la mémoire est consommée sur votre appareil mobile uniquement si vous redimensionnez le navigateur aux bonnes dimensions. Si vous déboguiez ou testiez des environnements iPhone, redimensionnez-les à 480 x 320 px. Nous comprenons désormais exactement comment fonctionne l'accélération matérielle et ce qu'il faut pour la déboguer. C'est une chose de lire à ce sujet, mais voir réellement les tampons de mémoire GPU fonctionner visuellement permet de mieux comprendre les choses.

En coulisses : récupération et mise en cache

Il est maintenant temps de passer à la vitesse supérieure en matière de mise en cache des pages et des ressources. Comme l'approche utilisée par JQuery Mobile et les frameworks similaires, nous allons précharger et mettre en cache nos pages avec des appels AJAX simultanés.

Examinons quelques problèmes fondamentaux liés au Web mobile et les raisons pour lesquelles nous devons les résoudre :

  • Récupération : le préchargement de nos pages permet aux utilisateurs de mettre l'application hors connexion et d'éviter d'attendre entre les actions de navigation. Bien sûr, nous ne voulons pas saturer la bande passante de l'appareil lorsqu'il se connecte. Nous devons donc utiliser cette fonctionnalité avec parcimonie.
  • Mise en cache : nous souhaitons ensuite une approche simultanée ou asynchrone lors de la récupération et de la mise en cache de ces pages. Nous devons également utiliser localStorage (car il est bien pris en charge par les appareils), qui n'est malheureusement pas asynchrone.
  • AJAX et analyse de la réponse : l'utilisation de innerHTML() pour insérer la réponse AJAX dans le DOM est dangereuse (et peu fiable ?). Nous utilisons plutôt un mécanisme fiable pour l'insertion de réponses AJAX et la gestion des appels simultanés. Nous utilisons également de nouvelles fonctionnalités HTML5 pour analyser le xhr.responseText.

En nous basant sur le code de la démonstration de diapositives, de retournements et de rotations, nous commençons par ajouter des pages secondaires et à créer des liens vers celles-ci. Nous analyserons ensuite les liens et créerons des transitions à la volée.

iPhone Home

Cliquez ici pour voir la démonstration de la récupération et de la mise en cache.

Comme vous pouvez le voir, nous utilisons ici le balisage sémantique. Un simple lien vers une autre page. La page enfant suit la même structure de nœuds/classes que sa page parente. Nous pourrions aller plus loin et utiliser l'attribut data-* pour les nœuds "page", etc. Voici la page de détails (enfant) située dans un fichier HTML distinct (/demo2/home-detail.html) qui sera chargé, mis en cache et configuré pour la transition lors du chargement de l'application.

<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>

Examinons maintenant le code JavaScript. Par souci de simplicité, je ne vais pas inclure d'assistants ni d'optimisations dans le code. Ici, nous ne faisons que parcourir un tableau spécifié de nœuds DOM pour extraire les liens à récupérer et à mettre en cache. Remarque : Pour cette démo, la méthode fetchAndCache() est appelée au chargement de la page. Nous allons le retravailler dans la section suivante lorsque nous détecterons la connexion réseau et déterminerons quand il doit être appelé.

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();
      }
    }
  }
};

Nous assurons un post-traitement asynchrone approprié grâce à l'utilisation de l'objet "AJAX". Pour une explication plus avancée de l'utilisation de localStorage dans un appel AJAX, consultez Working Off the Grid with HTML5 Offline. Dans cet exemple, vous voyez l'utilisation de base de la mise en cache sur chaque requête et la fourniture des objets mis en cache lorsque le serveur renvoie une réponse autre qu'une réponse réussie (200).

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;
      }
    }
  }
}

Malheureusement, comme localStorage utilise UTF-16 pour l'encodage des caractères, chaque octet est stocké sous forme de 2 octets, ce qui réduit notre limite de stockage de 5 Mo à 2,6 Mo au total. La raison pour laquelle ces pages/balises sont récupérées et mises en cache en dehors du champ d'application du cache d'application sera expliquée dans la section suivante.

Grâce aux récents progrès de l'élément iframe avec HTML5, nous disposons désormais d'un moyen simple et efficace d'analyser le responseText que nous obtenons en retour de notre appel AJAX. Il existe de nombreux analyseurs JavaScript et expressions régulières de 3 000 lignes qui suppriment les balises de script, etc. Mais pourquoi ne pas laisser le navigateur faire ce qu'il fait de mieux ? Dans cet exemple, nous allons écrire le responseText dans un iFrame masqué temporaire. Nous utilisons l'attribut HTML5 "sandbox" qui désactive les scripts et offre de nombreuses fonctionnalités de sécurité…

Extrait de la spécification : L'attribut "sandbox", lorsqu'il est spécifié, permet d'appliquer un ensemble de restrictions supplémentaires à tout contenu hébergé par l'iframe. Sa valeur doit être un ensemble non ordonné de jetons uniques séparés par des espaces, qui ne sont pas sensibles à la casse ASCII. Les valeurs autorisées sont "allow-forms", "allow-same-origin", "allow-scripts" et "allow-top-navigation". Lorsque l'attribut est défini, le contenu est considéré comme provenant d'une origine unique, les formulaires et les scripts sont désactivés, les liens ne peuvent pas cibler d'autres contextes de navigation et les plug-ins sont désactivés.

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 refuse correctement de déplacer implicitement un nœud d'un document vers un autre. Une erreur est générée si le nouveau nœud enfant a été créé dans un autre document. Ici, nous utilisons adoptNode et tout se passe bien.

Alors, pourquoi utiliser un iFrame ? Pourquoi ne pas simplement utiliser innerHTML ? Même si innerHTML fait désormais partie de la spécification HTML5, il est dangereux d'insérer la réponse d'un serveur (malveillant ou non) dans une zone non vérifiée. Au moment de la rédaction de cet article, je n'ai trouvé personne qui utilisait autre chose que innerHTML. Je sais que jQuery l'utilise au cœur de son fonctionnement avec un ajout de secours en cas d'exception uniquement. JQuery Mobile l'utilise également. Cependant, je n'ai pas effectué de tests approfondis concernant l'arrêt aléatoire du fonctionnement d'innerHTML, mais il serait très intéressant de voir toutes les plates-formes concernées. Il serait également intéressant de voir quelle approche est la plus performante… J'ai entendu des affirmations des deux côtés à ce sujet.

Détection, gestion et profilage du type de réseau

Maintenant que nous pouvons mettre en mémoire tampon (ou en cache prédictif) notre application Web, nous devons fournir les fonctionnalités de détection de connexion appropriées qui rendent notre application plus intelligente. C'est là que le développement d'applications mobiles devient extrêmement sensible aux modes en ligne/hors connexion et à la vitesse de connexion. Saisissez The Network Information API. Chaque fois que je présente cette fonctionnalité, quelqu'un dans le public lève la main et demande "À quoi ça sert ?". Voici donc une façon possible de configurer une application Web mobile extrêmement intelligente.

Commençons par un scénario de bon sens ennuyeux… Lorsque vous interagissez avec le Web depuis un appareil mobile dans un train à grande vitesse, il est fort possible que le réseau disparaisse à différents moments et que différentes zones géographiques prennent en charge différentes vitesses de transmission (par exemple, La technologie HSPA ou 3G peut être disponible dans certaines zones urbaines, mais les zones rurales peuvent prendre en charge des technologies 2G beaucoup plus lentes. Le code suivant couvre la plupart des scénarios de connexion.

Le code suivant fournit :

  • Accès hors connexion via applicationCache.
  • Détecte si le fichier est mis en favoris et hors connexion.
  • Détecte le passage du mode hors connexion au mode en ligne et inversement.
  • Détecte les connexions lentes et récupère le contenu en fonction du type de réseau.

Encore une fois, toutes ces fonctionnalités nécessitent très peu de code. Tout d'abord, nous détectons nos événements et nos scénarios de chargement :

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);

Dans les EventListeners ci-dessus, nous devons indiquer à notre code s'il est appelé à partir d'un événement, d'une demande de page ou d'une actualisation. La raison principale est que l'événement onload du corps ne sera pas déclenché lors du passage du mode en ligne au mode hors connexion.

Ensuite, nous effectuons une simple vérification pour un événement ononline ou onload. Ce code réinitialise les liens désactivés lors du passage du mode hors connexion au mode en ligne. Toutefois, si cette application était plus sophistiquée, vous pourriez insérer une logique qui reprendrait la récupération de contenu ou gérerait l'UX pour les connexions intermittentes.

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;
    }
  }
}

Il en va de même pour processOffline(). Vous pouvez manipuler votre application pour le mode hors connexion et essayer de récupérer les transactions qui se déroulaient en arrière-plan. Le code ci-dessous extrait tous nos liens externes et les désactive, piégeant les utilisateurs dans notre application hors connexion POUR TOUJOURS, mouhahaha !

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);

    }
  }
}

Passons maintenant à la partie qui nous intéresse le plus. Maintenant que notre application connaît son état de connexion, nous pouvons également vérifier le type de connexion lorsqu'elle est en ligne et l'ajuster en conséquence. J'ai indiqué les débits de téléchargement et les latences des fournisseurs nord-américains typiques dans les commentaires de chaque connexion.

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);
  }
}

Nous pourrions apporter de nombreuses modifications à notre processus fetchAndCache, mais je me suis contenté ici de lui indiquer de récupérer les ressources de manière asynchrone (true) ou synchrone (false) pour une connexion donnée.

Chronologie des requêtes Edge (synchrones)

Synchronisation Edge

Chronologie des demandes WIFI (asynchrones)

WIFI Async

Cela permet d'ajuster l'expérience utilisateur en fonction de la vitesse de connexion. Il ne s'agit en aucun cas d'une solution universelle. Une autre tâche à faire serait d'afficher une boîte de dialogue de chargement lorsqu'un lien est cliqué (sur les connexions lentes), tandis que l'application peut toujours récupérer la page de ce lien en arrière-plan. L'objectif principal est de réduire la latence tout en exploitant pleinement les capacités de la connexion de l'utilisateur avec les dernières fonctionnalités HTML5. Cliquez ici pour voir la démonstration de la détection du réseau.

Conclusion

Le voyage sur la route des applications HTML5 mobiles ne fait que commencer. Vous avez maintenant une idée des bases très simples d'un "framework" mobile construit uniquement autour de HTML5 et de ses technologies associées. Je pense qu'il est important que les développeurs travaillent avec ces fonctionnalités et les traitent à leur base, et non masquées par un wrapper.