Técnicas HTML5 para otimizar o desempenho em dispositivos móveis

Introdução

Atualizações com a roda de carregamento, transições de página irregulares e atrasos periódicos em eventos de toque são apenas algumas das dores de cabeça nos ambientes da Web para dispositivos móveis. Os desenvolvedores tentam se aproximar o máximo possível do nativo, mas muitas vezes são prejudicados por invasões, redefinições e estruturas rígidas.

Neste artigo, discutiremos o mínimo do que é necessário para criar um aplicativo da Web HTML5 para dispositivos móveis. O ponto principal é desmascarar as complexidades ocultas que as estruturas móveis atuais tentam ocultar. Você vai encontrar uma abordagem minimalista (usando APIs principais do HTML5) e fundamentos básicos que vão permitir que você escreva seu próprio framework ou contribua com o que você usa atualmente.

Aceleração de hardware

Normalmente, as GPUs lidam com modelagem 3D detalhada ou diagramas CAD, mas, neste caso, queremos que nossos desenhos primitivos (divs, planos de fundo, texto com sombras projetadas, imagens etc.) apareçam suaves e tenham animação suave pela GPU. O problema é que a maioria dos desenvolvedores front-end está encaminhando esse processo de animação para um framework de terceiros sem se preocupar com a semântica, mas será que esses recursos principais do CSS3 precisam ser mascarados? Confira alguns motivos para se preocupar com essas coisas:

  1. Alocação de memória e carga computacional: se você for compor todos os elementos no DOM apenas para aceleração de hardware, a próxima pessoa que trabalhar no seu código poderá perseguir você e dar uma surra.

  2. Consumo de energia: obviamente, quando o hardware entra em ação, a bateria também entra. Ao desenvolver para dispositivos móveis, os desenvolvedores precisam considerar uma ampla variedade de restrições de dispositivos ao criar apps da Web para dispositivos móveis. Isso será ainda mais comum quando os fabricantes de navegadores começarem a permitir o acesso a cada vez mais dispositivos de hardware.

  3. Conflitos: encontrei um comportamento com falhas ao aplicar a aceleração de hardware a partes da página que já estavam aceleradas. Portanto, saber se há aceleração sobreposta é muito importante.

Para que a interação do usuário seja suave e o mais nativa possível, precisamos fazer com que o navegador funcione para nós. O ideal é que a CPU do dispositivo móvel configure a animação inicial e que a GPU seja responsável apenas por compor diferentes camadas durante o processo de animação. É isso que o translate3d, o scale3d e o translateZ fazem: eles dão aos elementos animados a própria camada, permitindo que o dispositivo renderize tudo de forma suave. Para saber mais sobre a composição acelerada e como o WebKit funciona, Ariya Hidayat tem muitas informações boas no blog dele.

Transições de página

Vamos conferir três das abordagens de interação do usuário mais comuns ao desenvolver um app da Web para dispositivos móveis: efeitos de deslizar, virar e girar.

Você pode ver esse código em ação aqui http://slidfast.appspot.com/slide-flip-rotate.html (observação: esta demonstração foi criada para um dispositivo móvel, portanto, inicie um emulador, use seu telefone ou tablet ou reduza o tamanho da janela do navegador para ~1024 px ou menos).

Primeiro, vamos analisar as transições de deslizamento, rotação e rotação e como elas são aceleradas. Observe que cada animação requer apenas três ou quatro linhas de CSS e JavaScript.

Deslizante

A mais comum das três abordagens de transição, as transições de página deslizante imitam a sensação nativa de aplicativos para dispositivos móveis. A transição de slides é invocada para trazer uma nova área de conteúdo para a porta de visualização.

Para o efeito de deslizar, primeiro declaramos nossa marcação:

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

Perceba como temos este conceito de páginas de preparação à esquerda ou à direita. Pode ser qualquer direção, mas é a mais comum.

Agora temos animação e aceleração de hardware com apenas algumas linhas de CSS. A animação real acontece quando trocamos classes nos elementos div da página.

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

translate3d(0,0,0) é conhecida como a abordagem da "bala de prata".

Quando o usuário clica em um elemento de navegação, executamos o seguinte JavaScript para trocar as classes. Nenhum framework de terceiros está sendo usado. É JavaScript puro! ;)

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 se torna stage-center e força a página a deslizar para a janela de visualização central. Dependemos totalmente do CSS3 para fazer o trabalho pesado.

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

.stage-right {
  left: 480px;
}

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

Em seguida, vamos conferir o CSS que lida com a detecção e a orientação do dispositivo móvel. É possível lidar com todos os dispositivos e resoluções. Consulte resolução de consulta de mídia. Usei apenas alguns exemplos simples nesta demonstração para cobrir a maioria das visualizações em modo retrato e paisagem em dispositivos móveis. Isso também é útil para aplicar aceleração de hardware por dispositivo. Por exemplo, como a versão para computador do WebKit acelera todos os elementos transformados (sejam eles 2D ou 3D), faz sentido criar uma consulta de mídia e excluir a aceleração nesse nível. Observe que os truques de aceleração de hardware não oferecem nenhum aumento de velocidade no Android Froyo 2.2+. Toda a composição é feita dentro do 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;
  }
}

Inversão

Em dispositivos móveis, a virada é conhecida como deslizar a página. Aqui, usamos um JavaScript simples para processar esse evento em dispositivos iOS e Android (com base no WebKit).

Confira em ação http://slidfast.appspot.com/slide-flip-rotate.html.

Ao lidar com eventos de toque e transições, a primeira coisa que você vai querer é controlar a posição atual do elemento. Consulte este documento para obter mais informações sobre WebKitCSSMatrix.

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

Como estamos usando uma transição de CSS3 para a virada de página, o element.offsetLeft normal não vai funcionar.

Em seguida, queremos descobrir para qual direção o usuário está invertendo e definir um limite para que um evento (navegação na página) ocorra.

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

Observe que também estamos medindo o swipeTime em milissegundos. Isso permite que o evento de navegação seja acionado se o usuário deslizar rapidamente a tela para virar uma página.

Para posicionar a página e fazer com que as animações pareçam nativas enquanto um dedo toca na tela, usamos transições CSS3 após cada disparo de evento.

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

Tentei brincar com bezier cúbica para dar a melhor sensação nativa às transições, mas o facilidade-out conseguiu.

Por fim, para fazer a navegação acontecer, precisamos chamar os métodos slideTo() definidos anteriormente que usamos na última demonstração.

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

Como girar

Em seguida, vamos analisar a animação de rotação usada nesta demonstração. A qualquer momento, você pode girar a página que está sendo mostrada em 180 graus para revelar o verso tocando na opção de menu "Contato". Novamente, isso requer apenas algumas linhas de CSS e JavaScript para atribuir uma classe de transição onclick. OBSERVAÇÃO: a transição de rotação não é renderizada corretamente na maioria das versões do Android porque não tem recursos de transformação 3D do CSS. Infelizmente, em vez de ignorar a virada, o Android faz a página "girar", em vez de virar. Recomendamos usar essa transição com moderação até que o suporte melhore.

A marcação (conceito básico de frente e verso):

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

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

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

Como depurar a aceleração de hardware

Agora que abordamos nossas transições básicas, vamos dar uma olhada na mecânica de como elas funcionam e são compostas.

Para fazer essa sessão de depuração mágica acontecer, vamos ativar alguns navegadores e o ambiente de desenvolvimento integrado que você escolher. Primeiro, inicie o Safari na linha de comando para usar algumas variáveis de ambiente de depuração. Estou usando um Mac, então os comandos podem ser diferentes dependendo do SO. Abra o Terminal e digite o seguinte:

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

Isso inicia o Safari com alguns auxiliares de depuração. CA_COLOR_OPAQUE mostra quais elementos são realmente compostos ou acelerados. CA_LOG_MEMORY_USAGE mostra quanta memória estamos usando ao enviar nossas operações de exibição para o armazenamento de backup. Isso informa exatamente a quantidade de esforço que você está colocando no dispositivo móvel e pode dar dicas de como o uso da GPU pode estar esgotando a bateria do dispositivo de destino.

Agora vamos abrir o Chrome para conferir algumas informações úteis sobre frames por segundo (QPS):

  1. Abra o navegador da Web Google Chrome.
  2. Na barra de URL, digite about:flags.
  3. Role alguns itens para baixo e clique em "Ativar" no contador de QPS.

Se você acessar esta página na versão turbinada do Chrome, o contador de QPS vermelho vai aparecer no canto superior esquerdo.

QPS do Chrome

É assim que sabemos que a aceleração de hardware está ativada. Ele também nos dá uma ideia de como a animação é executada e se você tem vazamentos (animações em execução contínua que precisam ser interrompidas).

Outra maneira de visualizar a aceleração de hardware é abrir a mesma página no Safari (com as variáveis de ambiente mencionadas acima). Todos os elementos DOM acelerados têm uma tonalidade vermelha. Isso mostra exatamente o que está sendo composto por camada. Observe que a navegação branca não fica vermelha porque não está acelerada.

Contato composto

Uma configuração semelhante para o Chrome também está disponível em about:flags, em "Bordas de camada de renderização composta".

Outra ótima maneira de conferir as camadas compostas é acessar a demonstração de folhas caindo do WebKit com esse mod aplicado.

Folhas compostas

Por fim, para entendermos melhor o desempenho do hardware gráfico do nosso aplicativo, vamos analisar como a memória está sendo consumida. Aqui vemos que estamos enviando 1,38 MB de instruções de desenho para os buffers do CoreAnimation no Mac OS. Os buffers de memória da animação principal são compartilhados entre o OpenGL ES e a GPU para criar os pixels finais que aparecem na tela.

Coreanimation 1

Quando simplesmente redimensionamos ou maximizamos a janela do navegador, a memória também aumenta.

Animação principal 2

Isso dá uma ideia de como a memória está sendo consumida no dispositivo móvel, mas apenas se você redimensionar o navegador para as dimensões corretas. Se você estava depurando ou testando para ambientes do iPhone, redimensione para 480 x 320 pixels. Agora entendemos exatamente como a aceleração de hardware funciona e o que é necessário para depurar. É uma coisa ler sobre isso, mas ver os buffers de memória da GPU funcionando visualmente realmente coloca as coisas em perspectiva.

Bastidores: busca e armazenamento em cache

Agora é hora de levar o armazenamento em cache de páginas e recursos para o próximo nível. Assim como a abordagem usada pelo JQuery Mobile e frameworks semelhantes, vamos fazer uma pré-busca e armazenar em cache nossas páginas com chamadas AJAX simultâneas.

Vamos abordar alguns problemas principais da Web para dispositivos móveis e os motivos para fazer isso:

  • Busca: a busca antecipada das nossas páginas permite que os usuários desconectem o app e também não precisam esperar entre as ações de navegação. É claro que não queremos prejudicar a largura de banda do dispositivo quando ele estiver on-line. Portanto, use esse recurso com moderação.
  • Cache: em seguida, queremos uma abordagem simultânea ou assíncrona ao buscar e armazenar em cache essas páginas. Também precisamos usar localStorage, já que ele é compatível com vários dispositivos, que, infelizmente, não é assíncrono.
  • AJAX e análise da resposta: usar innerHTML() para inserir a resposta AJAX no DOM é perigoso (e não confiável?). Em vez disso, usamos um mecanismo confiável para inserção de respostas AJAX e processamento de chamadas simultâneas. Também usamos alguns novos recursos do HTML5 para analisar o xhr.responseText.

Ao criar o código da demonstração Deslizar, Inverter e Girar, começamos adicionando algumas páginas secundárias e vinculando-as a elas. Em seguida, vamos analisar os links e criar transições em tempo real.

iPhone Home

Confira a demonstração de busca e armazenamento em cache aqui.

Como você pode ver, estamos usando a marcação semântica aqui. Apenas um link para outra página. A página filha segue a mesma estrutura de nó/classe da página mãe. Podemos ir além e usar o atributo data-* para nós "página" etc. E aqui está a página de detalhes (filha) localizada em um arquivo HTML separado (/demo2/home-detail.html), que será carregado, armazenado em cache e configurado para transição no carregamento do app.

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

Agora vamos conferir o JavaScript. Para simplificar, estou deixando auxiliares ou otimizações de fora do código. Aqui, estamos fazendo um loop em uma matriz especificada de nós DOM para encontrar links para buscar e armazenar em cache. Observação: para esta demonstração, o método fetchAndCache() está sendo chamado no carregamento da página. Trabalharemos isso novamente na próxima seção, quando detectarmos a conexão de rede e determinarmos quando ela deve ser chamada.

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

Garantimos o pós-processamento assíncrono adequado usando o objeto "AJAX". Há uma explicação mais avançada sobre o uso do localStorage em uma chamada AJAX em Como trabalhar fora da rede com o HTML5 off-line. Neste exemplo, você vai conferir o uso básico do armazenamento em cache em cada solicitação e fornecer os objetos armazenados em cache quando o servidor retornar qualquer coisa, exceto uma resposta bem-sucedida (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;
      }
    }
  }
}

Infelizmente, como localStorage usa UTF-16 para codificação de caracteres, cada byte é armazenado como 2 bytes, aumentando nosso limite de armazenamento de 5 MB para 2,6 MB no total. O motivo de buscar e armazenar em cache essas páginas/marcações fora do escopo do cache do aplicativo é revelado na próxima seção.

Com os avanços recentes no elemento iframe com HTML5, agora temos uma maneira simples e eficaz de analisar o responseText que recebemos de volta da nossa chamada AJAX. Há muitos analisadores JavaScript de 3.000 linhas e expressões regulares que removem tags de script e assim por diante. Mas por que não deixar o navegador fazer o que ele faz de melhor? Neste exemplo, vamos gravar o responseText em um iframe oculto temporário. Estamos usando o atributo "sandbox" do HTML5, que desativa scripts e oferece muitos recursos de segurança…

De acordo com a especificação: O atributo sandbox, quando especificado, ativa um conjunto de restrições extras em qualquer conteúdo hospedado pelo iframe. O valor precisa ser um conjunto não ordenado de tokens únicos separados por espaços que são ASCII e não diferenciam maiúsculas de minúsculas. Os valores permitidos são allow-forms, allow-same-origin, allow-scripts e allow-top-navigation. Quando o atributo é definido, o conteúdo é tratado como se fosse de uma origem única, formulários e scripts são desativados, os links não podem segmentar outros contextos de navegação e os plug-ins são desativados.

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

O Safari se recusa corretamente a mover um nó de um documento para outro de forma implícita. Um erro será gerado se o novo nó filho for criado em um documento diferente. Aqui, usamos adoptNode e está tudo bem.

Por que usar o iframe? Por que não usar innerHTML? Embora o innerHTML agora faça parte da especificação HTML5, é uma prática perigosa inserir a resposta de um servidor (bom ou ruim) em uma área não verificada. Durante a escrita deste artigo, não encontrei ninguém usando nada além do innerHTML. Sei que o JQuery usa isso nativamente com uma exceção de fallback de apêndice. E o jQuery Mobile também usa. No entanto, não fiz testes pesados em relação ao innerHTML "parar de funcionar aleatoriamente", mas seria muito interessante saber quais plataformas isso afeta. Também seria interessante saber qual abordagem tem melhor desempenho. Já ouvi alegações de ambos os lados sobre isso.

Detecção, tratamento e criação de perfil de rede

Agora que podemos armazenar em buffer (ou armazenar em cache preditivo) nosso aplicativo da web, devemos fornecer os recursos de detecção de conexão adequados que tornam nosso aplicativo mais inteligente. É aqui que o desenvolvimento de apps para dispositivos móveis fica extremamente sensível aos modos on-line/off-line e à velocidade de conexão. Insira a API Network Information. Sempre que mostro esse recurso em uma apresentação, alguém na plateia levanta a mão e pergunta: "Para que eu usaria isso?". Então, aqui está uma maneira possível de configurar um app da Web para dispositivos móveis extremamente inteligente.

Primeiro, um cenário comum e chato: ao interagir com a Web em um dispositivo móvel em um trem de alta velocidade, a rede pode ficar indisponível em vários momentos, e diferentes regiões podem oferecer velocidades de transmissão diferentes (por exemplo, O HSPA ou 3G pode estar disponível em algumas áreas urbanas, mas as áreas remotas podem oferecer suporte a tecnologias 2G muito mais lentas. O código a seguir aborda a maioria dos cenários de conexão.

O código a seguir fornece:

  • Acesso off-line pelo applicationCache.
  • Detecta se a página está marcada como favorita e off-line.
  • Detecta quando a mudança é feita de off-line para on-line e vice-versa.
  • Detecta conexões lentas e busca conteúdo com base no tipo de rede.

Novamente, todos esses recursos exigem muito pouco código. Primeiro detectamos nossos eventos e cenários de carregamento:

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

Nos EventListeners acima, precisamos informar ao nosso código se ele está sendo chamado em um evento ou em uma solicitação ou atualização de página real. O motivo principal é que o evento onload do corpo não é acionado ao alternar entre os modos on-line e off-line.

Em seguida, temos uma verificação simples de um evento ononline ou onload. Esse código redefine links desativados ao alternar do modo off-line para o on-line, mas, se esse app fosse mais sofisticado, você poderia inserir uma lógica que retomasse a busca de conteúdo ou processasse a UX para conexões intermitentes.

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

O mesmo vale para processOffline(). Aqui, você manipula o app para o modo off-line e tenta recuperar as transações que estavam acontecendo nos bastidores. O código abaixo explora todos os nossos links externos e os desativa, prendendo os usuários em nosso aplicativo off-line PARA SEMPRE!

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

    }
  }
}

Certo, vamos ao que interessa. Agora que o app sabe em que estado está conectado, também podemos verificar o tipo de conexão quando está on-line e fazer os ajustes necessários. Listei nos comentários de cada conexão os downloads e as latências mais comuns dos provedores da América do Norte.

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

Há vários ajustes que podemos fazer no processo fetchAndCache, mas tudo o que fiz aqui foi pedir para buscar os recursos assíncronos (verdadeiro) ou síncronos (falso) para uma determinada conexão.

Linha do tempo das solicitações de borda (síncronas)

Edge Sync

Cronograma de solicitação de Wi-Fi (assíncrono)

Wi-Fi assíncrono

Isso permite pelo menos algum método de ajuste da experiência do usuário com base em conexões lentas ou rápidas. Essa não é uma solução definitiva. Outra tarefa seria abrir um modal de carregamento quando um link é clicado (em conexões lentas), enquanto o app ainda pode buscar a página desse link em segundo plano. O objetivo é reduzir a latência e aproveitar ao máximo os recursos da conexão do usuário com as melhores tecnologias do HTML5. Confira a demonstração de detecção de rede aqui.

Conclusão

A jornada dos apps HTML5 para dispositivos móveis está apenas começando. Agora você conhece os fundamentos simples e básicos de um "framework" para dispositivos móveis criado exclusivamente com HTML5 e tecnologias compatíveis. Acho que é importante que os desenvolvedores trabalhem com esses recursos e abordem esses recursos em seu núcleo e não mascarados por um wrapper.