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

Introdução

Atualizações giratórias, transições de página instáveis e atrasos periódicos em eventos de toque são apenas alguns dos problemas nos ambientes da Web para dispositivos móveis atuais. Os desenvolvedores tentam chegar o mais perto possível do nativo, mas muitas vezes são prejudicados por hacks, redefinições e frameworks rígidos.

Neste artigo, vamos discutir o mínimo necessário para criar um web app HTML5 para dispositivos móveis. O principal objetivo é revelar as complexidades ocultas que os frameworks para dispositivos móveis atuais tentam esconder. Você vai conhecer uma abordagem minimalista (usando APIs HTML5 principais) e fundamentos básicos que vão permitir que você escreva seu próprio framework ou contribua com o que usa atualmente.

Aceleração de hardware

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

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

  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 a grande variedade de restrições de dispositivos ao escrever web apps para dispositivos móveis. Isso vai ficar ainda mais evidente quando os fabricantes de navegadores começarem a permitir o acesso a mais e mais hardware de dispositivos.

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

Para tornar a interação do usuário suave e o mais próxima possível da nativa, precisamos fazer o navegador trabalhar 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 translate3d, scale3d e translateZ fazem: eles dão aos elementos animados a própria camada, permitindo que o dispositivo renderize tudo junto sem problemas. 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 analisar três das abordagens mais comuns de interação do usuário ao desenvolver um web app para dispositivos móveis: efeitos de deslizar, virar e girar.

Confira este 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 smartphone ou tablet ou reduza o tamanho da janela do navegador para aproximadamente 1024 px ou menos.

Primeiro, vamos analisar as transições de slide, inversão e rotação e como elas são aceleradas. Observe como cada animação usa 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 deslizantes imitam a sensação nativa dos aplicativos móveis. A transição de slide é invocada para trazer uma nova área de conteúdo para a janela de visualização.

Para o efeito de slide, 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 esse conceito de páginas de teste à esquerda ou à direita. Pode ser qualquer direção, mas essa é 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 as 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 de "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. Estamos dependendo completamente do CSS3 para fazer o trabalho pesado.

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

.stage-right {
  left: 480px;
}

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

Agora vamos conferir o CSS, que processa a detecção e a orientação de dispositivos móveis. Podemos abordar todos os dispositivos e resoluções (consulte resolução de consulta de mídia). Usei apenas alguns exemplos simples nesta demonstração para abranger a maioria das visualizações 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. Os truques de aceleração de hardware não oferecem melhoria de velocidade no Android Froyo 2.2 ou versões mais recentes. Toda a composição é feita no 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, virar é conhecido como deslizar a página para fora. Aqui, usamos um JavaScript simples para processar esse evento em dispositivos iOS e Android (baseados em WebKit).

Confira na prática 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 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 ease-out do CSS3 para a inversão de página, o element.offsetLeft usual não vai funcionar.

Em seguida, precisamos descobrir em qual direção o usuário está deslizando e definir um limite para que um evento (navegação de 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;
  }
}

Você também vai notar que 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 usar cubic-bezier para dar a melhor sensação nativa às transições, mas ease-out funcionou.

Por fim, para que a navegação aconteça, precisamos chamar os métodos slideTo() definidos anteriormente e 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 conferir a animação de rotação usada nesta demonstração. A qualquer momento, você pode girar a página que está visualizando em 180 graus para revelar o outro lado tocando na opção de menu "Contato". Isso leva apenas algumas linhas de CSS e um pouco de 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 CSS 3D. Infelizmente, em vez de ignorar a inversão, o Android faz com que a página "gire" ao invés de inverter. 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);
}

Depuração da aceleração de hardware

Agora que já abordamos as transições básicas, vamos analisar a mecânica de como elas funcionam e são compostas.

Para que essa sessão de depuração mágica aconteça, vamos abrir alguns navegadores e o ambiente de desenvolvimento integrado da sua escolha. Primeiro, inicie o Safari na linha de comando para usar algumas variáveis de ambiente de depuração. Estou no Mac, então os comandos podem variar de acordo com seu SO. Abra o terminal e digite o seguinte:

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

Isso inicia o Safari com alguns assistentes 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 desenho para o repositório de apoio. Isso informa exatamente o quanto você está forçando o dispositivo móvel e pode dar dicas de como o uso da GPU pode estar consumindo a bateria do dispositivo de destino.

Agora, vamos abrir o Chrome para conferir algumas informações boas sobre frames por segundo (FPS):

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

Se você acessar esta página na versão aprimorada do Chrome, vai ver o contador de FPS vermelho 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 há 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 está vermelha porque não é acelerada.

Contato composto

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

Outra ótima maneira de ver as camadas compostas é acessar a demonstração de folhas caindo do WebKit enquanto essa modificação está aplicada.

omposited Leaves

Por fim, para entender de verdade 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 do Core Animation 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, também vemos a memória aumentar.

Coreanimation 2

Isso dá uma ideia de como a memória está sendo consumida no dispositivo móvel apenas se você redimensionar o navegador para as dimensões corretas. Se você estiver depurando ou testando para ambientes iPhone, redimensione para 480 px por 320 px. 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 traz as coisas para a perspectiva.

Bastidores: busca e armazenamento em cache

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

Vamos abordar alguns problemas principais da Web móvel e os motivos para isso:

  • Busca: a pré-busca das nossas páginas permite que os usuários usem o app off-line e não precisem esperar entre as ações de navegação. É claro que não queremos prejudicar a largura de banda do dispositivo quando ele ficar on-line. Por isso, precisamos usar 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 o localStorage, já que ele é bem compatível com dispositivos, mas não é assíncrono.
  • AJAX e análise da resposta: usar innerHTML() para inserir a resposta do 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.

Com base no código da demonstração de deslizar, girar e rotacionar, começamos adicionando algumas páginas secundárias e vinculando a elas. Em seguida, vamos analisar os links e criar transições imediatamente.

Casa do iPhone

Confira a demonstração de busca e 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 um pouco mais longe e usar o atributo data-* para nós "page", etc. Esta é 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 analisar o JavaScript. Para simplificar, estou deixando de fora do código qualquer helper ou otimização. Aqui, estamos apenas percorrendo uma matriz especificada de nós do DOM para encontrar links a serem buscados e armazenados em cache. Observação: para esta demonstração, o método fetchAndCache() está sendo chamado no carregamento da página. Vamos refazer isso 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 de localStorage em uma chamada AJAX em Trabalhar sem conexão com a Internet com HTML5 Offline. Neste exemplo, você vê o uso básico do armazenamento em cache em cada solicitação e o fornecimento dos objetos armazenados em cache quando o servidor retorna algo diferente de 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 o localStorage usa UTF-16 para codificação de caracteres, cada byte único é armazenado como 2 bytes, reduzindo 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 da nossa chamada AJAX. Há muitos analisadores de 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 temporário oculto. Estamos usando o atributo "sandbox" do HTML5, que desativa scripts e oferece muitos recursos de segurança…

Na especificação: Quando especificado, o atributo sandbox 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 exclusivos separados por espaços que não diferenciam maiúsculas de minúsculas em ASCII. 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 sendo de uma origem única, os formulários e scripts são desativados, os links são impedidos de 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 implicitamente um nó de um documento para outro. Um erro será gerado se o novo nó filho tiver sido criado em um documento diferente. Então, aqui usamos adoptNode e tudo fica bem.

Então, por que usar iframes? Por que não usar apenas innerHTML? Embora innerHTML agora faça parte da especificação HTML5, é uma prática perigosa inserir a resposta de um servidor (malicioso ou não) em uma área não verificada. Durante a escrita deste artigo, não encontrei ninguém usando outra coisa além de innerHTML. Sei que o JQuery usa isso no núcleo com um retorno de anexação somente em exceção. O JQuery Mobile também usa. No entanto, não fiz nenhum teste pesado em relação ao innerHTML "para de funcionar aleatoriamente", mas seria muito interessante ver todas as plataformas afetadas. Também seria interessante saber qual abordagem tem melhor performance. Já ouvi declarações dos dois lados sobre isso.

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

Agora que podemos armazenar em buffer (ou cache preditivo) nosso web app, precisamos fornecer os recursos adequados de detecção de conexão para torná-lo mais inteligente. É aqui que o desenvolvimento de apps para dispositivos móveis se torna extremamente sensível aos modos on-line/off-line e à velocidade da 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 web app para dispositivos móveis extremamente inteligente.

Primeiro, um cenário de senso comum sem graça… Ao interagir com a Web em um dispositivo móvel em um trem de alta velocidade, a rede pode cair em vários momentos, e diferentes regiões geográficas podem oferecer suporte a 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 o conteúdo está salvo nos favoritos e off-line.
  • Detecta quando você muda de off-line para on-line e vice-versa.
  • Detecta conexões lentas e busca conteúdo com base no tipo de rede.

Mais uma vez, 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 de um evento ou de uma solicitação ou atualização de página real. O principal motivo é que o evento onload do corpo não será 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 os links desativados ao alternar do modo off-line para on-line, mas, se o app fosse mais sofisticado, você poderia inserir uma lógica que retomasse a busca de conteúdo ou processasse a experiência do usuário 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ê manipularia o app para o modo off-line e tentaria recuperar as transações que estavam acontecendo nos bastidores. O código abaixo extrai todos os nossos links externos e os desativa, prendendo os usuários no nosso app off-line PARA SEMPRE, muahaha!

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

    }
  }
}

OK, vamos para a parte boa. Agora que nosso app sabe em que estado de conexão ele está, também podemos verificar o tipo de conexão quando ele está on-line e ajustar de acordo. Listei nos comentários de cada conexão as latências e velocidades de download típicas 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 dizer para buscar os recursos de forma assíncrona (true) ou síncrona (false) para uma determinada conexão.

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

Sincronização de borda

Linha do tempo de solicitação de Wi-Fi (assíncrona)

WIFI Async

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 mostrar um modal de carregamento quando um link é clicado (em conexões lentas), enquanto o app ainda pode estar buscando a página desse link em segundo plano. O principal objetivo aqui é reduzir as latências e aproveitar todos os recursos da conexão do usuário com o que há de mais recente e melhor no HTML5. Confira a demonstração de detecção de rede aqui.

Conclusão

A jornada no mundo dos apps HTML5 para dispositivos móveis está apenas começando. Agora você vê os fundamentos muito simples e básicos de um "framework" para dispositivos móveis criado apenas com HTML5 e tecnologias de suporte. Acho importante que os desenvolvedores trabalhem e abordem esses recursos no núcleo, não mascarados por um wrapper.