WinJS no Windows 8.1

Crie aplicativos da Windows Store mais eficientes usando JavaScript: Desempenho

Eric Schmidt

Baixar o código de exemplo

Ao explorar como construir aplicativos mais eficientes para a Windows Store, foquei primeiramente no tratamento de erros. Neste segundo artigo, vou examinar várias técnicas para melhorar o desempenho de um aplicativo da Windows Store, focando no uso de memória e na capacidade de resposta da IU HTML. Vou apresentar o novo modelo de ciclo de vida de objeto previsível na Biblioteca do Windows para JavaScript no Windows 8.1 (WinJS 2.0). Depois, vou examinar Web Workers e a nova API Scheduler no WinJS 2.0, ambos executam tarefas em segundo plano sem bloquear a IU. Como no artigo anterior, vou apresentar duas ferramentas de diagnóstico para localizar os problemas e as soluções para resolver os problemas descobertos.

Presumo que você esteja bem familiarizado com a criação de aplicativos para a Windows Store usando JavaScript. Se você for relativamente novo na plataforma, sugiro começar com o exemplo básico “Olá, mundo” (bit.ly/vVbVHC) ou, para aumentar o desafio, a amostra do “Hilo” para JavaScript (bit.ly/SgI0AA). Se você não leu o artigo anterior, poderá encontrá-lo em msdn.microsoft.com/magazine/dn519922.

Configurando o exemplo

Ao longo deste artigo, apresentarei exemplos específicos que você pode testar em seu próprio código. Você pode acompanhar ou baixar o código completo para examinar quando quiser.

Estou usando casos de teste diferentes do artigo anterior, então, você pode querer adicionar novos botões à barra de navegação global, se você estiver acompanhando. (Você pode simplesmente começar um projeto de aplicativo de navegação totalmente novo, se preferir, o que funciona também.) As novas NavBarCommands são mostradas na Figura 1.

Figura 1 NavBarCommands adicionais em Default.html

<div data-win-control="WinJS.UI.NavBar">
  <div data-win-control="WinJS.UI.NavBarContainer">
    <!-- Other NavBarCommand elements. -->
    <div id="dispose"
      data-win-control="WinJS.UI.NavBarCommand"
      data-win-options="{
        location: '/pages/dispose/dispose.html',
        icon: 'delete',
        label: 'Dispose pattern in JS'
    }">
    </div>
    <div id="scheduler"
      data-win-control="WinJS.UI.NavBarCommand"
      data-win-options="{
        location: '/pages/scheduler/scheduler.html',
        icon: 'clock',
        label: 'Scheduler'
    }">
    </div>
    <div id="worker"
      data-win-control="WinJS.UI.NavBarCommand"
      data-win-options="{
        location: '/pages/worker/worker.html',
        icon: 'repair',
        label: 'Web worker'
    }">
    </div>
  </div>
</div>

Para estes casos de teste, eu uso o cenário mais realista de um aplicativo que apresenta conteúdo da Web. Este aplicativo obtém dados do serviço Web de catálogo online de impressos e fotografias da biblioteca de congresso dos Estados Unidos (1.usa.gov/1d8nEio). Escrevi um módulo que oculta chamadas para o serviço Web em objetos promise e define classes para armazenar os dados recebidos. A Figura 2mostra o módulo em um arquivo intitulado searchLOC.js (/js/searchLOC.js).

Figura 2 Acessando o serviço Web de catálogo online de impressos e fotografias

(function () {
  "use strict";
  var baseUrl = "http://loc.gov/pictures/"
  var httpClient = new Windows.Web.Http.HttpClient();
  function searchPictures(query) {
    var url = baseUrl + "search/?q=" + query + "&fo=json";
    var queryURL = encodeURI(url);
    return httpClient.getStringAsync(
      new Windows.Foundation.Uri(queryURL)).
      then(function (response) {
        return JSON.parse(response).results.map(function (result) {
          return new SearchResult(result);
        });
     });
  }
  function getCollections() {
    var url = baseUrl + "?fo=json";
    return httpClient.getStringAsync(new Windows.Foundation.Uri(url)).
      then(function (response) {
         return JSON.parse(response).featured.
           map(function (collection) {
             return new Collection(collection);
         });
      });
  }
  function getCollection(collection) {
    var url = baseUrl + "search/?co=" + collection.code + "&fo=json";
    var queryUrl = encodeURI(url);
    return httpClient.getStringAsync(new Windows.Foundation.Uri(queryurl)).
      then(function (response) {
        collection.pictures = JSON.parse(response).
          results.map(function (picture) {
            return new SearchResult(picture);
        });
        return collection;
      });
  }
  function Collection(info) {
    this.title = info.title;
    this.featuredThumb = info.thumb_featured;
    this.code = info.code;
    this.pictures = [];
  }
  function SearchResult(data) {
    this.pictureThumb = data.image.thumb;
    this.title = data.title;
    this.date = data.created_published_date;
  }
  WinJS.Namespace.define("LOCPictures", {
    Collection: Collection,
    searchPictures: searchPictures,
    getCollections: getCollections,
    getCollection: getCollection
  });
})();

Lembre-se de vincular ao arquivo searchLOC.js de default.html na raiz do seu projeto antes de tentar chamá-lo.

Descarte de objetos

No JavaScript, um objeto permanece na memória contanto que possa ser alcançado por um ambiente lexical ou uma cadeia de referências. Quando todas as referências ao objeto forem removidas, o coletor de lixo desaloca memória do objeto. Enquanto uma referência ao objeto permanecer, o objeto fica na memória. Um vazamento de memória ocorre se uma referência a um objeto (e, portanto, o objeto em si) permanece por mais tempo quando é necessário.

Uma causa comum dos vazamentos de memória em aplicativos JavaScript são objetos "zumbis", que normalmente ocorrem quando um objeto JavaScript faz referência a um objeto DOM e esse objeto DOM é removido do documento (por meio de uma chamada para removeChild ou innerHTML). O objeto JavaScript correspondente permanece na memória, mesmo que o HTML correspondente desapareça:

var newSpan = document.createElement("span");
document.getElementById("someDiv").appendChild(newSpan);
document.getElementById("someDiv").innerHTML = "";
WinJS.log && WinJS.log(newSpan === "undefined");
// The previous statement outputs false to the JavaScript console.
// The variable "newSpan" still remains even though the corresponding
// DOM object is gone.

Para uma página Web normal, a vida útil de um objeto se estende apenas ao tempo que o navegador exibe a página. Os aplicativos da Windows Store não podem ignorar esses tipos de vazamentos de memória. Aplicativos geralmente usam uma única página HTML como um host de conteúdo, onde essa página persiste durante toda a sessão de aplicativo (que pode durar dias ou até meses). Se um aplicativo mudar de estado (o usuário navegar de uma página para outra, por exemplo, ou um controle ListView for rolado de forma que alguns itens saiam de visibilidade) sem limpar a memória alocada para objetos JavaScript desnecessários, essa memória poderá se tornar disponível para o aplicativo.

Verificação de vazamentos de memória

Felizmente, o Visual Studio 2013 tem novos recursos que podem ajudar os desenvolvedores a rastrearem vazamentos de memória, especificamente a janela Desempenho e Diagnóstico. Para este caso de teste e no próximo, vou demonstrar algumas das ferramentas envolvidas.

Para este caso de teste, vou adicionar um controle personalizado à minha solução que propositadamente permite vazamentos de memória. Esse controle, chamado SearchLOCControl (/js/SearchLOCControl.js), cria uma caixa de texto de pesquisa e exibe os resultados depois de uma resposta a uma consulta ser recebida. A Figura 3 mostra o código para SearchLOCControl.js. Mais uma vez, lembre-se de vincular a esse novo arquivo JavaScript de default.html.

Figura 3 SearchLOCControl personalizado

(function () {
  "use strict";
  WinJS.Namespace.define("SearchLOCControl", {
    Control: WinJS.Class.define(function (element) {
      this.element = element;
      this.element.winControl = this;
      var htmlString = "<h3>Library of Congress Picture Search</h3>" +
        "<div id='searchQuery' data-win-control='WinJS.UI.SearchBox'" +
          "data-win-options='{ placeholderText: \"Browse pictures\" }'></div>" +
          "<br/><br/>" +
          "<div id='searchResults' class='searchList'></div>" +
          "<div id='searchResultsTemplate'" +
            "data-win-control='WinJS.Binding.Template'>" +
            "<div class='searchResultsItem'>" +
              "<img src='#' data-win-bind='src: pictureThumb' />" +
              "<div class='details'>" +
                "<p data-win-bind='textContent: title'></p>" +
                "<p data-win-bind='textContent: date'></p>" +
              "</div>" +
            "</div>"+
        "</div>";
   // NOTE: This is an unusual technique for accomplishing this
   // task. The code here is written for extreme brevity.       
      MSApp.execUnsafeLocalFunction(function () {
        $(element).append(htmlString);
        WinJS.UI.processAll();
      });
      this.searchQuery = $("#searchQuery")[0];
      searchQuery.winControl.addEventListener("querysubmitted", this.submitQuery);
      }, {
        submitQuery: function (evt) {
          var queryString = evt.target.winControl.queryText;
          var searchResultsList = $("#searchResults")[0];
          $(searchResultsList).append("<progress class='win-ring'></progress>");
          if (queryString != "") {
            var searchResults = LOCPictures.searchPictures(queryString).
              then(function (response) {
                var searchList = new WinJS.Binding.List(response),
                  searchListView;
                if (searchResultsList.winControl) {
                  searchListView = searchResultsList.winControl;
                  searchListView.itemDataSource = searchList.dataSource;
                }
                else {
                  searchListView = new WinJS.UI.ListView(searchResultsList, {
                    itemDataSource: searchList.dataSource,
                    itemTemplate: $("#searchResultsTemplate")[0],
                    layout: { type: WinJS.UI.CellSpanningLayout}
                  });
                }
                WinJS.UI.process(searchListView);
             });
           }
         }
      })
   })
})();

Observe que eu uso jQuery para construir meu controle personalizado, que adiciono à minha solução usando o Gerenciador de Pacotes NuGet. Depois de baixar o pacote NuGet para sua solução, é preciso adicionar manualmente uma referência à biblioteca jQuery em default.html.

O SearchLOCControl depende de certa estilização que eu adicionei a default.css (/css/default.css), que é mostrada na Figura 4.

Figura 4 Estilização adicionada a Default.css

.searchList {
  height: 700px !important;
  width: auto !important;
}
.searchResultsItem {
  display: -ms-inline-grid;
  -ms-grid-columns: 200px;
  -ms-grid-rows: 150px 150px
}
  .searchResultsItem img {
    -ms-grid-row: 1;
    max-height: 150px;
    max-width: 150px;
  }
  .searchResultsItem .details {
    -ms-grid-row: 2;
  }

Agora adiciono um novo controle de página chamado dispose.html (pages/dispose/dispose.html) à solução e adiciono a seguinte marcação HTML dentro da marca <section> de descarte para criar o controle personalizado:

<button id="dispose">Dispose</button><br/><br/>
<div id="searchControl" data-win-control="SearchLOCControl.Control"></div>

Por fim, adiciono o código ao manipulador de eventos PageControl.ready no arquivo dispose.js (/pages/dispose/dispose.js) que, ingenuamente, destrói o controle e cria um vazamento de memória, definindo o innerHTML do host <div> do controle para uma cadeia de caracteres vazia, como mostrado na Figura 5.

Figura 5 Código em Dispose.js para "destruir” o controle personalizado

(function () {
  "use strict";
  WinJS.UI.Pages.define("/pages/dispose/dispose.html", {
    ready: function (element, options) {
      WinJS.UI.processAll();
      $("#dispose").click(function () {
        var searchControl = $("#searchControl")[0];
        searchControl.innerHTML = "";
      });
    }
  // Other page control code.
  });
})();

Agora posso testar o uso de memória do controle. A janela Desempenho e Diagnóstico fornece várias ferramentas para medir o desempenho de um aplicativo da Windows Store, incluindo a amostragem de CPU, o consumo de energia do aplicativo, a capacidade de resposta da interface do usuário e o tempo da função JavaScript. (Você pode ler mais sobre essas ferramentas no blog da equipe do Visual Studio em bit.ly/1bESdOH.) Se já não estiver visível, será preciso abrir o painel Desempenho e Diagnóstico pelo menu Depurar (Visual Studio Express 2013 para Windows) ou pelo menu Analisar (Visual Studio Professional 2013 e Visual Studio Ultimate 2013).

Para este teste, uso a ferramenta de monitoramento de memória JavaScript. Estas são as etapas para executar o teste:

  1. Na janela Desempenho e Diagnóstico, selecione Memória JavaScript e clique em Iniciar. O projeto é executado no modo de depuração. Se solicitado pela caixa de diálogo Controle de Conta de Usuário, clique em Sim.
  2. Com o projeto de aplicativo em execução, navegue até a página de descarte e alterne para a área de trabalho. No Visual Studio, na sessão de diagnóstico atual (uma guia intitulada "Report*.diagsession"), clique em Obter Instantâneo de Heap.
  3. Volte para o aplicativo em execução. Na caixa de pesquisa, digite uma consulta (por exemplo, "Lincoln") e pressione Enter. Um controle ListView aparece exibindo os resultados de pesquisa de imagem.
  4. Volte para a área de trabalho. No Visual Studio, na sessão de diagnóstico atual (uma guia intitulada "Report*.diagsession"), clique em Obter Instantâneo de Heap.
  5. Volte para o aplicativo em execução. Clique no botão Descartar. O controle personalizado desaparece da página.
  6. Volte para a área de trabalho. No Visual Studio, na sessão de diagnóstico atual (uma guia intitulada "Report*.diagsession"), clique em Obter Instantâneo de Heap e em Parar. Agora há três instantâneos listados na sessão de diagnóstico, como mostrado na Figura 6.

Memory Usage Before Implementing the Dispose Pattern
Figura 6 Uso de memória antes de implementar o padrão de descarte

Com os dados de diagnóstico em mãos, posso analisar o uso de memória do controle personalizado. Em uma olhada rápida na sessão de diagnóstico, suspeito que "descartar" o controle não libera toda a memória associada a ele.

No relatório, posso examinar os objetos JavaScript no heap para cada instantâneo. Quero saber o que restou na memória depois que o controle personalizado foi removido do DOM. Vou clicar no link associado ao número de objetos no heap no terceiro instantâneo (Instantâneo 3 na Figura 6).

Primeiro vou verificar a exibição Dominadores, que mostra uma lista dos objetos classificados por tamanho retido. Os objetos que consomem mais memória e que são potencialmente mais fáceis de liberar estão listados no topo. Na exibição Dominadores, vejo uma referência ao <div> com um valor de ID "searchControl". Ao expandi-lo, vejo que a caixa de pesquisa, ListView e os dados associados estão todos na memória.

Quando clico com o botão direito do mouse na linha do <div> searchControl e seleciono Mostrar na exibição de raiz, vejo que os manipuladores de eventos para os cliques de botões ainda estão na memória também, como a Figura 7 mostra.

Unattached Event Handler Code Taking up Memory
Figura 7 Código do manipulador de eventos desanexado consumindo memória

Felizmente, posso consertar isso facilmente com algumas alterações no meu código.

Implementando o padrão de descarte no WinJS

No WinJS 2.0, todos os controles WinJS implementam um padrão de "descarte" para resolver o problema de vazamentos de memória. Sempre que um controle WinJS sai do escopo (por exemplo, quando o usuário navega para outra página), o WinJS limpa todas as referências a ele. O controle é marcado para descarte, o que significa que o coletor de lixo interno sabe liberar toda a memória alocada ao objeto.

O padrão de descarte no WinJS tem três características importantes que um controle deve fornecer para ser devidamente descartado:

  • O elemento DOM do contêiner de nível superior deve ter a classe CSS “win-disposable”.
  • A classe do controle deve incluir um campo chamado _disposed que é inicialmente definido como false. Você pode adicionar esse membro a um controle (juntamente com a classe CSS win-disposable) chamando WinJS.Utilities.markDisposable.
  • A classe JavaScript que define o controle deve expor um método “dispose”. No método dispose:
    • Toda a memória alocada aos objetos associados ao controle precisa ser liberada.
    • Todos os manipuladores de eventos precisam ser desanexados dos objetos DOM filho.
    • Todos os filhos do controle devem ter seus métodos dispose chamados. A melhor maneira de fazer isso é chamando WinJS.Utilities.disposeSubTree no elemento host.
    • Todas as promessas pendentes que podem ser referenciadas no controle precisam ser canceladas (chamando o método Promise.cancel e anulando a variável out).

Então, na função de construtor de SearchLOCControl.Control, adiciono as seguintes linhas de código:

this._disposed = false;
WinJS.Utilities.addClass(element, "win-disposable");

Em seguida, dentro da definição da classe SearchLOCControl (a chamada para WinJS.Class.define), adiciono um novo membro de instância chamado dispose. Este é o código para o método dispose:

dispose: function () {
  this._disposed = true;
  this.searchQuery.winControl.removeEventListener("querysubmitted",
    this.submitQuery);
  WinJS.Utilities.disposeSubTree(this.element);
  this.searchQuery = null;
  this._element.winControl = null;
  this._element = null;
}

De um modo geral, não é preciso limpar qualquer variável no código que está inteiramente contido dentro do código do elemento. Quando o código do controle desaparece, todas as variáveis internas também desaparecem. No entanto, se o código do controle fizer referência a algo fora dele mesmo, como um elemento DOM, por exemplo, essa referência deverá ser anulada.

Finalmente, adiciono uma chamada explícita ao método dispose em dispose.js (/pages/dispose/dispose.js). Aqui está o manipulador de eventos de clique atualizado para o botão em dispose.html:

$("#dispose").click(function () {
  var searchControl = $("#searchControl")[0];
  searchControl.winControl.dispose();
  searchControl.innerHTML = "";
});

Agora, quando executo o mesmo teste de memória JavaScript, a sessão de diagnóstico parece muito melhor (veja a Figura 8).

Memory Usage After Implementing Dispose
Figura 8 Uso de memória depois de implementar o descarte

Examinando o heap de memória, posso ver que o "searchControl" <div> não tem mais elementos filho associados (veja a Figura 9). Nenhum dos subcontroles permanecem na memória e os manipuladores de eventos associados desapareceram também (veja a Figura 10).

Dominators View After Implementing Dispose
Figura 9 Exibição Dominadores após a implementação do descarte

Roots View After Implementing Dispose
Figura 10 Exibição Raízes após a implementação do descarte

Melhorando a capacidade de resposta: Scheduler e Web Workers

Os aplicativos podem não responder quando a interface do usuário está aguardando para ser atualizado com base em um processo externo. Por exemplo, se um aplicativo faz várias solicitações a um serviço Web para preencher um controle de interface do usuário, o controle – toda interface do usuário, para esse assunto – pode ficar preso enquanto aguarda as solicitações. Isso pode fazer com que o aplicativo "gagueje" ou pareça não responder.

Para demonstrar isso, crio outro caso de teste onde preencho um controle Hub com as "coleções de destaque" fornecidas pelo serviço Web da Biblioteca do Congresso. Adiciono um novo controle de página chamado scheduler.html para o caso de teste ao meu projeto (/pages/scheduler/scheduler.js). No HTML da página, declaro um controle Hub que contém seis controles HubSection (um para cada coleção de destaque). O HTML para o controle Hub dentro das marcas <section> em scheduler.html é mostrado na Figura 11.

Figura 11 Controles Hub e HubSection declarados em Scheduler.html

<div id="featuredHub" data-win-control="WinJS.UI.Hub">
  <div data-win-control="WinJS.UI.HubSection"
    data-win-options="{
      header: 'Featured Collection 1'
    }"
    class="section">
  </div>
  <div data-win-control="WinJS.UI.HubSection"
    data-win-options="{
      header: 'Featured Collection 2'
    }"
    class="section">
  </div>
  <div data-win-control="WinJS.UI.HubSection"
    data-win-options="{
      header: 'Featured Collection 3'
    }"
    class="section">
  </div>
  <div data-win-control="WinJS.UI.HubSection"
    data-win-options="{
      header: 'Featured Collection 4'
    }"
    class="section">
  </div>
  <div data-win-control="WinJS.UI.HubSection"
    data-win-options="{
      header: 'Featured Collection 5'
    }"
    class="section">
  </div>
  <div data-win-control="WinJS.UI.HubSection"
    data-win-options="{
      header: 'Featured Collection 6'
    }"
    class="section">
  </div>
</div>

Em seguida, recebo os dados das coleções de destaque do serviço Web. Vou adicionar um novo arquivo chamado data.js à minha solução (/js/data.js) que chama o serviço Web e retorna um objeto WinJS.Binding.List. A Figura 12 mostra o código para obter os dados das coleções em destaque. Mais uma vez, lembre-se de vincular ao data.js de default.html.

Figura 12 Obtendo os dados do serviço Web

(function () {
  "use strict";
  var data = LOCPictures.getCollections().
  then(function (message) {
    var data = message;
    var dataList = new WinJS.Binding.List(data);
    var collectionTasks = [];
    for (var i = 0; i < 6; i++) {
      collectionTasks.push(getFeaturedCollection(data[i]));
    }
    return WinJS.Promise.join(collectionTasks).then(function () {
      return dataList;
    });
  });
  function getFeaturedCollection(collection) {
    return LOCPictures.getCollection(collection);
  }
 WinJS.Namespace.define("Data", {
   featuredCollections: data
 });
})();

Agora eu preciso inserir os dados no controle Hub. No arquivo scheduler.js (/pages/scheduler/scheduler.js), vou adicionar um código à função PageControl.ready e definir uma nova função, populateSection. O código completo é mostrado na Figura 13.

Figura 13 Preenchendo o controle Hub dinamicamente

(function () {
  "use strict";
  var dataRequest;
  WinJS.UI.Pages.define("/pages/scheduler/scheduler.html", {
    ready: function (element, options) {
      performance.mark("navigated to scheduler");
      dataRequest = Data.featuredCollections.
        then(function (collections) {
          performance.mark("got collection");
          var hub = element.querySelector("#featuredHub");
            if (!hub) { return; }
            var hubSections = hub.winControl.sections,
            hubSection, collection;
            for (var i = 0; i < hubSections.length; i++) {
              hubSection = hubSections.getItem(i);
              collection = collections.getItem(i);
              populateSection(hubSection, collection);
            }
        });
    },
    unload: function () {
      dataRequest.cancel();
    }
    // Other PageControl members ...
  });
  function populateSection(section, collection) {
    performance.mark("creating a hub section");
    section.data.header = collection.data.title;
    var contentElement = section.data.contentElement;
    contentElement.innerHTML = "";
    var pictures = collection.data.pictures;
    for (var i = 0; i < 6; i++) {
      $(contentElement).append("<img src='" 
        + pictures[i].pictureThumb + "' />");
      (i % 2) && $(contentElement).append("<br/>")
    }
    }
})();

Observe na Figura 13 que eu capturo uma referência ao promise retornado pela chamada a Data.getFeaturedCollections e depois cancelo explicitamente o promise quando a página descarrega. Isso evita uma possível condição de corrida em um cenário onde o usuário navega para a página e, em seguida, sai dela antes de a chamada a getFeaturedCollections retornar.

Quando pressiono F5 e navego até scheduler.html, percebo que o controle Hub é preenchido lentamente após a página ser carregada. Pode ser um pouco irritante no meu computador, mas em computadores menos potentes o atraso pode ser significativo.

Visual Studio 2013 contém ferramentas para mensurar a capacidade de resposta da interface do usuário em um aplicativo da Windows Store. No painel Desempenho e Diagnóstico, seleciono o teste Capacidade de Resposta da Interface de Usuário HTML e clico em Iniciar. Depois que o aplicativo começa a ser executado, navego para scheduler.html e observo os resultados aparecerem no controle Hub. Depois de concluir a tarefa, volto para a área de trabalho e clico em Parar na guia da sessão de diagnóstico. A Figura 14 mostra os resultados.

HTML UI Responsiveness for Scheduler.html
Figura 14 Capacidade de Resposta da Interface de Usuário HTML para Scheduler.html

Vejo que a taxa de quadros caiu para 3 FPS em cerca de meio segundo. Seleciono o período de baixa taxa de quadros para ver mais detalhes (veja a Figura 15).

Timeline Details Where the UI Thread Evaluates Scheduler.js
Figura 15 A linha do tempo detalha onde o thread da IU avalia o Scheduler.js

Neste ponto da linha do tempo (Figura 15), o thread da IU é absorvido na execução de scheduler.js. Se você observar atentamente os detalhes da linha do tempo, verá várias marcas de usuário (marcas de seleção em laranja). Elas indicam chamadas específicas para performance.mark no código. Em scheduler.js, a primeira chamada para performance.mark ocorre quando o scheduler.html é carregado. Preencher cada controle HubSection com conteúdo invoca uma chamada subsequente. Nos resultados, mais da metade do tempo gasto na avaliação de scheduler.js ocorreu entre quando naveguei para a página (a primeira marca de usuário) e quando a sexta HubSection foi preenchida com imagens (a última marca de usuário).

(Tenha em mente que os resultados variam dependendo do seu hardware. Os testes de capacidade de resposta da interface do usuário HTML apresentados neste artigo foram executados em um Microsoft Surface Pro com um processador Intel Core i5-3317U de terceira geração, em execução a 1,7 Ghz e Intel HD Graphics 400.)

Para reduzir o atraso, eu poderia refatorar meu código para que os controles HubSection sejam preenchidos de forma escalonada. Usuários veem o conteúdo no aplicativo logo depois que navegam até ele. O conteúdo para a primeira a duas seções do hub deve ser carregado imediatamente após a navegação e outras HubSections podem ser carregadas depois.

Scheduler

JavaScript é um ambiente de thread único, ou seja, tudo fica no thread da IU. No WinJS 2.0, a Microsoft introduziu o WinJS.Utilities.Scheduler para organizar o trabalho realizado no thread da IU (consulte bit.ly/1bFbpfb para obter mais informações).

O Scheduler cria uma única fila de trabalhos a serem executados no thread da IU no aplicativo. Os trabalhos são concluídos com base na prioridade, onde os trabalhos de maior prioridade podem antecipar ou adiar trabalhos de menor prioridade. Os trabalhos são programados com base na interação com o usuário real no thread da IU, onde o Scheduler corta o tempo entre as chamadas e conclui o máximo de trabalhos em fila possível.

Como mencionado, o programador executa trabalhos com base em sua prioridade, conforme definido usando a enumeração WinJS.Utilities.Scheduler.Priority. A enumeração tem sete valores (em ordem decrescente): max, high, aboveNormal, normal, belowNormal, idle e min. Os trabalhos de igual prioridade são executados por ordem de chegada.

Passando para o caso de teste, crio um trabalho no Scheduler para preencher cada HubSection quando o scheduler.html carrega. Para cada HubSection, chamo Scheduler.schedule e passo uma função que preenche a HubSection. Os dois primeiros trabalhos são executados em prioridade normal e todos os outros são executados quando o thread da IU está ocioso. No terceiro parâmetro para o método schedule, thisArg, passo algum contexto para o trabalho.

O método schedule retorna um objeto Job, o que me permite monitorar o andamento de um trabalho ou cancelá-lo. Para cada trabalho, atribuo o mesmo objeto OwnerToken à sua propriedade owner. Isso me permite cancelar todos os trabalhos agendados atribuídos a esse token owner. Veja a Figura 16.

Figura 16 Scheduler.js atualizado usando a API do Scheduler

(function () {
  "use strict";
  var dataRequest, jobOwnerToken;
  var scheduler = WinJS.Utilities.Scheduler;
  WinJS.UI.Pages.define("/pages/scheduler/scheduler.html", {
    ready: function (element, options) {
      performance.mark("navigated to scheduler");
      dataRequest = Data.featuredCollections.
        then(function (collections) {
          performance.mark("got collection");
          var hub = element.querySelector("#featuredHub");
          if (!hub) { return; }
          var hubSections = hub.winControl.sections,
          hubSection, collection, priority;
          jobOwnerToken = scheduler.createOwnerToken();
          for (var i = 0; i < hubSections.length; i++) {
            hubSection = hubSections.getItem(i);
            collection = collections.getItem(i);
            priority ==  (i < 2) ? scheduler.Priority.normal :
              scheduler.Priority.idle;
            scheduler.schedule(function () {
                populateSection(this.section, this.collection)
              },
              priority,
              { section: hubSection, collection: collection },
              "adding hub section").
            owner = jobOwnerToken;
          }
        });
      },
      unload: function () {
       dataRequest && dataRequest.cancel();
       jobOwnerToken && jobOwnerToken.cancelAll();
    }
    // Other PageControl members ...
  });
  function populateSection(section, collection) {
    performance.mark("creating a hub section");
    section.data.header = collection.data.title;
    var contentElement = section.data.contentElement;
    contentElement.innerHTML = "";
    var pictures = collection.data.pictures;
    for (var i = 0; i < 6; i++) {
      $(contentElement).append("<img src='" 
        + pictures[i].pictureThumb + "' />");
      (i % 2) && $(contentElement).append("<br/>")
    }
  }
})();

Agora, quando executo o teste de diagnóstico Capacidade de Resposta da Interface de Usuário HTML, eu deveria ver alguns resultados diferentes. A Figura 17 mostra os resultados do segundo teste.

HTML UI Responsiveness After Using Scheduler.js
Figura 17 Capacidade de Resposta da Interface de Usuário HTML depois de usar o Scheduler.js

Durante o segundo teste, o aplicativo ignorou menos quadros durante um curto período. A experiência no aplicativo foi melhor também: o controle Hub foi preenchido mais rapidamente e quase não houve atraso.

Manipular o DOM afeta a capacidade de resposta da IU

A adição de novos elementos ao DOM em uma página HTML pode prejudicar o desempenho, especialmente se você estiver for adicionar muitos elementos novos. A página precisa recalcular as posições dos outros itens da página, reaplicar estilos, e, por fim, repintar a página. Por exemplo, uma instrução CSS que define a parte superior, o lado esquerdo, largura, altura ou estilo de exibição de um elemento fará com que a página seja recalculada. (Recomendo usar os recursos de animação internos do WinJS ou as transformações de animação disponíveis em CSS3 para manipular a posição dos elementos HTML.)

No entanto, a injeção e exibição de conteúdo dinâmico é um design comum de aplicativo. Sua melhor opção para o desempenho, quando possível, é usar os dados de associação fornecidos pela plataforma. A associação de dados no WinJS é otimizada para uma UX rápida e responsiva.

Caso contrário, você terá de decidir entre injetar HTML bruto como uma cadeia de caracteres em outro elemento com innerHTML, ou adicionar elementos individuais um de cada vez usando createElement e appendChild. Usar innerHTML na maioria das vezes oferece um melhor desempenho, mas você pode não conseguir manipular o HTML depois de inserido.

Nos meus exemplos, escolhi o método $.append em jQuery. Ao acrescentar, posso passar o HTML bruto como cadeia de caracteres e obter acesso programático imediato aos novos nós do DOM. (Também oferece um desempenho muito bom.)

Web Workers

A plataforma Web padrão inclui a API do Web Worker que permite que um aplicativo execute tarefas em segundo plano fora do thread da IU. Em suma, Web Workers (ou apenas Workers) permitem o multithreading em aplicativos JavaScript. Você passa mensagens simples (uma cadeia de caracteres ou um simples objeto JavaScript) para o thread do Worker que, por sua vez, retorna mensagens para o thread principal usando o método postMessage.

Os Workers são executados em um contexto de script diferente do resto do aplicativo, então, eles não podem acessar a interface do usuário. Você não pode criar novos elementos HTML usando createElement ou usar recursos de bibliotecas de terceiros que dependem do objeto de documento (por exemplo, a função jQuery—$). No entanto, os Workers podem acessar as APIs do Tempo de Execução do Windows, o que significa que eles podem gravar dados de aplicativos, enviar atualizações de notificações e blocos, ou até mesmo salvar arquivos. Eles são ideais para tarefas de segundo plano que não precisam da intervenção do usuário, são computacionalmente caras ou exigem várias chamadas para um serviço Web. Se você quiser obter mais informações sobre a API do Web Worker, consulte a documentação de referência do Worker em bit.ly/1fllmip.

A vantagem de usar um thread do Worker é que a capacidade de resposta da interface do usuário não é afetada pelo trabalho em segundo plano. A interface do usuário permanece responsiva e praticamente nenhum quadro é descartado. Além disso, os Workers podem importar outras bibliotecas JavaScript que não dependem do DOM, incluindo a biblioteca fundamental para WinJS (base.js). Então, você pode, por exemplo, criar promises em um thread do Worker.

Por outro lado, os Workers não são a cura para todos os problemas de desempenho. Os ciclos para os threads do Worker ainda estão sendo alocados da totalidade dos ciclos da CPU disponíveis no computador, mesmo quando não são oriundos do thread da IU. Você precisa ser criterioso sobre o uso de Workers.

Para o próximo caso de teste, vou usar um thread do Worker para recuperar uma coleção de imagens da Biblioteca do Congresso e preencher um controle ListView com essas imagens. Primeiro, vou adicionar um novo script para armazenar o thread do Worker chamado LOC-worker.js ao meu projeto, como segue:

(function () {
  "use strict";
  self.addEventListener("message", function (message) {
    importScripts("//Microsoft.WinJS.2.0/js/base.js", "searchLoC.js");
    LOCPictures.getCollection(message.data).
      then(
        function (response) {
          postMessage(response);
        });
  });
})();

Uso a função importScripts para trazer base.js da biblioteca WinJS e os scripts seachLOC.js no contexto do Worker, tornando-os disponíveis para uso.

Em seguida, adiciono um novo item de controle de página chamado worker.html ao meu projeto (/pages/worker/worker.html). Acrescento um pouco de marcação dentro das marcas <section> em worker.html para conter o controle ListView e definir seu layout. O controle será criado dinamicamente quando o Worker retornar:

<div id="collection" class='searchList'>
  <progress class="win-ring"></progress>
</div>
<div id='searchResultsTemplate' data-win-control='WinJS.Binding.Template'>
  <div class='searchResultsItem'>
    <img src='#' data-win-bind='src: pictureThumb' />
    <div class='details'>
      <p data-win-bind='textContent: title'></p>
      <p data-win-bind='textContent: date'></p>
    </div>
  </div>
</div>

Finalmente, adiciono o código ao worker.js que cria um novo thread do Worker e, em seguida, preenche o HTML com base na resposta. O código no worker.js é exibido na Figura 18.

Figura 18 Criando um thread do Worker e preenchendo a IU

(function () {
  "use strict";
  WinJS.UI.Pages.define("/pages/worker/worker.html", {
    ready: function (element, options) {
      performance.mark("navigated to Worker");
      var getBaseballCards = new Worker('/js/LOC-worker.js'),
        baseballCards = new LOCPictures.Collection({
          title: "Baseball cards",
          thumbFeatured: null,
          code: "bbc"
      });
      getBaseballCards.onmessage = function (message) {
         createCollection(message.data);
         getBaseballCards.terminate();
      }
      getBaseballCards.postMessage(baseballCards);
    }
  // Other PageControl members ...
  });
  function createCollection(info) {
    var collection = new WinJS.Binding.List(info.pictures),
      collectionElement = $("# searchResultsTemplate")[0],
      collectionList = new WinJS.UI.ListView(collectionElement, {
        itemDataSource: collection.dataSource,
        itemTemplate: $('#collectionTemplate')[0],
        layout: {type: WinJS.UI.GridLayout}
      });
  }
})();

Quando você executa o aplicativo e navega até a página, perceberá um atraso mínimo entre a navegação e o preenchimento do controle ListView com imagens. Se você executar este caso de teste por meio da ferramenta de capacidade de resposta da IU HTML, verá um resultado semelhante ao que é mostrado na Figura 19.

HTML UI Responsiveness When Using a Worker ThreadFigura 19 Capacidade de Resposta da Interface de Usuário HTML ao usar o thread do Worker

Observe que o aplicativo descartou pouquíssimos quadros depois que eu naveguei para a página worker.html (após a primeira marca de usuário na linha do tempo). A interface do usuário permaneceu incrivelmente responsiva, pois a busca de dados foi descarregada para o thread do Worker.

Quando escolher entre as APIs do Scheduler e Worker

Como as APIs do Scheduler e do Worker permitem gerenciar tarefas em segundo plano em seu código, você pode estar se perguntando quando usar uma em vez da outra. (Observe que os dois exemplos de código que forneci neste artigo não são uma comparação justa e idêntica das duas APIs).

A API do Worker, como é executada em um thread diferente, proporciona melhor desempenho do que a do Scheduler em uma comparação "cabeça com cabeça". No entanto, como o Scheduler usa cortes de tempo do thread da IU, ele tem o contexto da página atual. Você pode usar o Scheduler para atualizar os elementos de interface do usuário em uma página ou criar dinamicamente novos elementos na página.

Se o seu código de segundo plano precisar interagir com a interface do usuário de qualquer forma significativa, use o Scheduler. No entanto, se seu código não depende do contexto do aplicativo e apenas passa dados simples para frente e para trás, use um Worker. A vantagem de usar um thread do Worker é que a capacidade de resposta da interface do usuário não é afetada pelo trabalho em segundo plano.

As APIs do Scheduler e do Web Worker não são suas únicas opções para a geração de vários threads em um aplicativo da Windows Store. Você também pode criar um componente do Tempo de Execução do Windows na linguagem C++, C# ou Visual Basic .NET, que pode criar novos threads. Os componentes do WinRT podem expor as APIs que o código JavaScript pode chamar. Para obter mais informações, consulte bit.ly/19DfFaO.

Duvido que muitos desenvolvedores começaram a escrever aplicativos cheios de bugs, falhas ou que não respondem (a menos que estejam escrevendo um artigo sobre aplicativos com essas características). O truque é encontrar os bugs no código de seu aplicativo e corrigi-los, de preferência antes que o aplicativo seja exposto aos usuários. Nesta série de artigos, demonstrei várias ferramentas para capturar problemas em seu código e técnicas para escrever código de aplicativo mais eficiente.

Claro que nenhuma dessas técnicas ou ferramentas são o milagre que resolverá automaticamente os problemas. Elas podem ajudar a melhorar a experiência, mas não eliminam a necessidade de boas práticas de codificação. Os fundamentos da programação, em geral, permanecem válidos para aplicativos da Window Store construídos com JavaScript como para qualquer outra plataforma.

Eric Schmidt é desenvolvedor de conteúdo na equipe de Conteúdo para desenvolvedores do Windows da Microsoft, escrevendo sobre a Biblioteca do Windows para JavaScript (WinJS). Quando trabalhava na divisão do Microsoft Office, ele criou exemplos de código para os aplicativos para a plataforma Office. Do contrário, ele passa seu tempo com a família, toca contrabaixo, cria videogames em HTML5 ou posta em blog sobre brinquedos de montar de plástico (historybricks.com).

AGRADECEMOS aos seguintes especialistas técnicos da Microsoft pela revisão deste artigo: Kraig Brockschmidt, Greg Bulmash e Josh Williams
Kraig Brockschmidt é um gerente de programa sênior da equipe de Ecossistema do Windows, trabalhando diretamente com a comunidade de desenvolvedores e parceiros-chave na criação de aplicativos para a Windows Store. Ele é autor do livro Programming Windows Store Apps in HTML, CSS, and JavaScript (já em sua segunda edição) e compartilha outros conhecimentos em http://www.kraigbrockschmidt.com/blog.

Josh Williams é engenheiro-chefe de desenvolvimento de software da equipe de Experiência de Desenvolvedor do Windows. Ele e sua equipe criaram a Biblioteca do Windows para JavaScript (WinJS).

Greg Bulmash escreve para o Centro de Desenvolvimento do Internet Explorer (https://msdn.microsoft.com/ie) e documenta as Ferramentas de Desenvolvedor F12 para IE11. Em seu tempo livre, ele tenta manter suas habilidades de codificação em dia e ajuda crianças a aprenderem a codificar organizando o capítulo de Seattle de CoderDojo. Ele escreve ocasionalmente no blog http://www.olddeveloper.com.