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

Wesley hales
Wesley Hales

Introdução

Atualizações cirúrgicas, transições de página entrecortadas 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 atuais. Os desenvolvedores tentam ficar o mais próximo possível do conteúdo nativo, mas muitas vezes acabam sendo impedidos por hackers, redefinições e estruturas rígidas.

Neste artigo, discutiremos o mínimo necessário para criar um aplicativo da Web HTML5 para dispositivos móveis. O ponto principal é desmascarar as complexidades que as estruturas para dispositivos móveis atuais tentam esconder. Você verá uma abordagem minimalista (usando as principais APIs HTML5) e os fundamentos básicos que permitirão que você crie sua própria estrutura ou contribua com a que você usa atualmente.

Aceleração de hardware

Normalmente, as GPUs lidam com 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 suaves e sejam animados com a GPU. Infelizmente, a maioria dos desenvolvedores front-end está enviando esse processo de animação a uma estrutura de terceiros sem se preocupar com a semântica. Mas será que esses recursos essenciais do CSS3 deveriam mascarar? Vou explicar alguns motivos pelos quais é importante se preocupar com essas coisas:

  1. Alocação de memória e carga computacional : se você compor cada elemento no DOM apenas para acelerar o hardware, a próxima pessoa que trabalhar no seu código poderá te perseguir e vencê-lo severamente.

  2. Consumo de energia: obviamente, quando o hardware é ligado, a bateria também começa. Ao desenvolver apps para dispositivos móveis, os desenvolvedores são forçados a considerar a grande variedade de restrições de dispositivos. Isso será ainda mais comum à medida que os fabricantes de navegadores começarem a permitir o acesso a cada vez mais hardwares de dispositivos.

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

Para facilitar a interação do usuário e o mais próximo possível do conteúdo nativo, precisamos fazer com que o navegador funcione para nós. Idealmente, queremos 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 sua própria camada, permitindo que o dispositivo renderize tudo sem problemas. Para saber mais sobre a composição acelerada e como o WebKit funciona, Ariya Hidayat tem muitas informações boas no blog.

Transições de página

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

Veja como esse código funciona aqui http://slidfast.appspot.com/slide-flip-rotate.html. Observação: essa demonstração foi criada para dispositivos móveis. Portanto, inicie um emulador, use seu smartphone ou tablet ou reduza o tamanho da janela do navegador para aproximadamente 1.024 pixels ou menos.

Primeiro, detalharemos as transições de deslizar, inverter e girar e como elas são aceleradas. Observe como cada animação ocupa apenas três ou quatro linhas de CSS e JavaScript.

Deslizante

A mais comum das três abordagens de transição, as transições deslizantes de página imitam a sensação nativa dos aplicativos para dispositivos móveis. A transição por slide é invocada para trazer uma nova área de conteúdo para a janela 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>

Observe como temos esse conceito de organizar páginas à esquerda ou à direita. Pode ser qualquer direção, mas é o 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 ideal.

Quando o usuário clica em um elemento de navegação, executamos o JavaScript a seguir para trocar as classes. Nenhuma estrutura de terceiros está sendo usada, isso é JavaScript direto! ;)

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. Nós dependemos totalmente do CSS3 para fazer o trabalho pesado.

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

.stage-right {
  left: 480px;
}

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

Agora vamos analisar o CSS que lida com a detecção e orientação de dispositivos móveis. É possível abordar cada dispositivo e resolução. Consulte Resolução da consulta de mídia. Usei alguns exemplos simples nesta demonstração para cobrir a maioria das visualizações de retrato e paisagem em dispositivos móveis. Isso também é útil para aplicar aceleração de hardware por dispositivo. Por exemplo, já que a versão desktop do WebKit acelera todos os elementos transformados (independentemente de serem 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 nenhuma melhoria de velocidade no Android Froyo 2.2 ou posterior. 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, a inversão é conhecida como deslizar a página para fora da tela. Aqui, usamos um JavaScript simples para manipular esse evento em dispositivos iOS e Android (baseados em WebKit).

Veja-o 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 é conseguir um identificador para a posição atual do elemento. Consulte este documento para obter mais informações sobre a 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 CSS3 facilitada para a virada de página, o element.offsetLeft padrão não funcionará.

Em seguida, queremos descobrir para qual direção o usuário está invertendo e definir um limite para que um evento (navegação nas páginas) 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 perceber que estamos medindo o swipeTime em milissegundos. Isso permite que o evento de navegação seja disparado se o usuário deslizar a tela rapidamente para virar uma página.

Para posicionar a página e fazer com que as animações pareçam nativas enquanto um dedo está tocando a tela, usamos transições CSS3 após o disparo de cada 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 bézier cúbica para dar a melhor sensação nativa às transições, mas a saída foi o suficiente.

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

Agora, vamos dar uma olhada na animação de rotação usada nesta demonstração. A qualquer momento, você pode girar a página que está visualizando no momento em 180 graus para mostrar o lado inverso tocando na opção de menu "Contato". Novamente, são necessárias 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 CSS 3D. 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);
}

Depurar a aceleração de hardware

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

Para fazer essa sessão mágica de depuração acontecer, vamos iniciar alguns navegadores e seu IDE de escolha. Primeiro, inicie o Safari na linha de comando para usar algumas variáveis de ambiente de depuração. Eu uso um Mac, então os comandos podem ser diferentes dependendo do seu sistema operacional. Abra o Terminal e digite o seguinte:

  • $> exportar CA_COLOR_OPAQUE=1
  • $> exportar 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 desenho para o backing store. Ela informa exatamente a carga no dispositivo móvel e pode dar dicas de como o uso da GPU pode estar drenando a bateria do dispositivo de destino.

Agora, vamos iniciar o Chrome para que possamos conferir algumas informações de quadros por segundo (QPS) bons:

  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" para o Contador de QPS.

Se você visualizar esta página na sua versão aprimorada do Chrome, verá o contador de QPS vermelho no canto superior esquerdo.

QPS do Chrome

É assim que sabemos que a aceleração de hardware está ativada. Isso também nos dá uma ideia de como a animação é executada e se há algum vazamento (animações em execução contínua que devem ser interrompidas).

Outra forma de visualizar a aceleração de hardware é abrir a mesma página no Safari (com as variáveis de ambiente que mencionei acima). Cada elemento DOM acelerado fica em vermelho. Isso nos mostra exatamente o que está sendo composto por camada. Observe que a navegação em branco 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 composta".

Outra ótima maneira de conferir as camadas compostas é conferir a demonstração de folhas com queda do WebKit enquanto a modificação é aplicada.

folhas opostas

E, por fim, para entender de verdade o desempenho do hardware gráfico do nosso aplicativo, vamos dar uma olhada em como a memória é consumida. Aqui observamos 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 você vê na tela.

Coreanimation 1

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

Coreanimation 2

Isso dá uma ideia de como a memória está sendo consumida no seu dispositivo móvel somente se você redimensionar o navegador para as dimensões corretas. Se você estiver depurando ou testando para ambientes do iPhone, redimensione para 480px por 320px. Agora entendemos exatamente como a aceleração de hardware funciona e o que é preciso para depurar. Uma coisa é ler sobre isso, mas ver os buffers de memória da GPU funcionando é bem o ponto de vista visual.

Nos bastidores: busca e armazenamento em cache

Chegou a hora de elevar o nível do armazenamento em cache da nossa página e de recursos. Assim como a abordagem usada pelo JQuery Mobile e por estruturas semelhantes, vamos fazer uma pré-busca e armazenar em cache nossas páginas com chamadas AJAX simultâneas.

Vamos abordar alguns dos principais problemas da Web para dispositivos móveis e as razões pelas quais precisamos fazer isso:

  • Busca: a pré-busca de nossas páginas permite que os usuários tenham o aplicativo off-line e também não precisa esperar entre as ações de navegação. Obviamente, não queremos sufocar a largura de banda do dispositivo quando ele ficar on-line, por isso precisamos usar esse recurso com moderação.
  • Armazenamento em cache: a seguir, queremos uma abordagem simultânea ou assíncrona ao buscar e armazenar em cache essas páginas. Também precisamos usar localStorage (já que é amplamente aceito entre os dispositivos), que infelizmente não é assíncrono.
  • AJAX e análise da resposta: o uso de innerHTML() para inserir a resposta AJAX no DOM é perigoso (e não confiável?). Em vez disso, usamos um mecanismo confiável para a inserção de resposta AJAX e o processamento de chamadas simultâneas. Também aproveitamos alguns novos recursos do HTML5 para analisar o xhr.responseText.

Com base no código da demonstração de deslizar, virar 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.

Página inicial do iPhone

Confira a demonstração da busca e do cache aqui.

Como você pode ver, estamos aproveitando 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. Poderíamos dar um passo adiante e usar o atributo data-* para nós de “página” etc. E esta é a página de detalhes (filho) localizada em um arquivo html separado (/demo2/home-detail.html) que será carregado, armazenado em cache e configurado para a 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, vou deixar os auxiliares ou otimizações de fora do código. Tudo o que estamos fazendo aqui é percorrer uma matriz especificada de nós DOM para procurar links para busca e armazenamento em cache. Observação: para esta demonstração, o método fetchAndCache() é chamado no carregamento da página. Vamos retrabalhar 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 um pós-processamento assíncrono adequado com o uso do objeto "AJAX". Há uma explicação mais avançada sobre o uso de localStorage em uma chamada AJAX em Como trabalhar fora da grade com HTML5 off-line (em inglês). Neste exemplo, você verá o uso básico do armazenamento em cache em cada solicitação e do fornecimento de objetos armazenados em cache quando o servidor retornar resultados diferentes 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 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. O motivo completo para 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 o HTML5, agora temos uma maneira simples e eficaz de analisar o responseText recebido da nossa chamada AJAX. Existem 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 oculto temporário. Estamos usando o atributo HTML5 “sandbox” que desativa scripts e oferece muitos recursos de segurança...

Pela 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 exclusivos separados por espaços que não diferenciam maiúsculas de minúsculas do 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 tendo uma origem exclusiva, formulários e scripts são desativados, 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 um nó implicitamente de um documento para outro. Um erro será gerado se o novo nó filho tiver sido criado em um documento diferente. Aqui, usamos adoptNode e tudo está bem.

Então, por que usar iframe? Por que não usar innerHTML? Embora 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 elaboração deste artigo, não encontrei ninguém usando nada além de innerHTML. Sei que JQuery a usa no seu núcleo com um substituto de anexo apenas como exceção. O JQuery Mobile também usa. No entanto, não fiz nenhum teste pesado em “para de funcionar de forma aleatória” do innerHTML, mas seria muito interessante ver todas as plataformas que isso afeta. Também seria interessante ver qual abordagem é mais eficiente... Eu já ouvi de ambos os lados sobre isso também.

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

Agora que temos a capacidade de armazenar em buffer (ou cache preditivo) nosso app da Web, precisamos fornecer os recursos adequados de detecção de conexão para tornar nosso aplicativo mais inteligente. É aqui que o desenvolvimento de aplicativos móveis fica extremamente sensível aos modos on-line/off-line e à velocidade da conexão. Insira A API Network Information. Toda vez que eu 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 possível maneira de configurar um aplicativo da Web para dispositivos móveis extremamente inteligente.

Primeiro, um cenário chato de bom senso... Ao interagir com a Web usando um dispositivo móvel em um trem de alta velocidade, a rede pode desaparecer em vários momentos, e diferentes regiões geográficas podem oferecer suporte a diferentes velocidades de transmissão (por exemplo, HSPA ou 3G podem estar disponíveis em algumas áreas urbanas, mas á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 via applicationCache.
  • Detecta se está nos favoritos e off-line.
  • Detecta quando muda 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 pouco código. Primeiro, detectamos nossos eventos e os 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);

No EventListeners acima, temos que informar ao nosso código se ele está sendo chamado a partir de um evento ou de uma solicitação ou atualização de página real. O principal motivo é que o evento do corpo onload 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 links desativados ao alternar de off-line para on-line, mas se o aplicativo fosse mais sofisticado, você poderia inserir uma lógica que retoma a busca de conteúdo ou gerencia 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ê manipularia seu aplicativo para o modo off-line e tentaria recuperar quaisquer transações que estavam em segundo plano. O código abaixo analisa todos os links externos e os desativa, prendendo usuários em nosso aplicativo off-line PARA SEMPRE 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);

    }
  }
}

OK, vamos para a parte boa. Agora que o app sabe em que estado da conexão está, também podemos verificar o tipo de conexão quando ele está on-line e fazer os ajustes necessários. Listei nos comentários de cada conexão os downloads e as latências típicos de 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 poderíamos fazer em nosso processo fetchAndCache, mas tudo o que eu fiz aqui foi para buscar os recursos assíncronos (verdadeiros) ou síncronos (falso) para uma determinada conexão.

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

Sincronização de borda

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

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 completa. Outra tarefa seria exibir 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 grande ponto aqui é reduzir as latências e, ao mesmo tempo, aproveitar todos os recursos que a conexão do usuário com o melhor e mais recente HTML5 tem a oferecer. Confira a demonstração de detecção de rede aqui.

Conclusão

A jornada pelos aplicativos HTML5 para dispositivos móveis só está começando. Agora você vê os alicerces simples e básicos de uma "estrutura" móvel construída unicamente em torno do HTML5 e suas tecnologias de suporte. Acho que é importante que os desenvolvedores trabalhem e lidem com esses recursos em sua essência, sem mascararem um wrapper.