WinJS в Windows 8.1

Создание более эффективных приложений Windows Store с применением JavaScript: производительность

Эрик Шмидт

Исходный код можно скачать по ссылке.

Продукты и технологии:

Windows 8.1, Windows Library for JavaScript 2.0, Visual Studio 2013

В статье рассматриваются:

  • подготовка приложения-примера, извлекающего контент из Web;
  • удаление объектов и проверка на утечку памяти;
  • реализация шаблона dispose в WinJS;
  • применение Scheduler API и Web Worker API.

Исследуя, как создавать более эффективные приложения Windows Store, я сначала рассмотрел обработку ошибок. Во второй статье мы обсудим несколько методов для повышения производительности приложения Windows Store, уделяя основное внимание использованию памяти и «отзывчивости» HTML UI. Я познакомлю вас с новой моделью предсказуемого жизненного цикла объектов (predictable object lifecycle model) из Windows Library for JavaScript в Windows 8.1 (WinJS 2.0). Затем мы изучим Web Workers и новый Scheduler API в WinJS 2.0, оба из которых выполняют фоновые задачи без блокировки UI. Как и в предыдущей статье, я представлю средства диагностики для поиска источников проблем и решения обнаруженных проблем.

Я буду исходить из того, что вы достаточно хорошо знакомы с созданием приложений Windows Store на JavaScript. Если эта платформа относительно нова для вас, предлагаю вам начать с базового примера «Hello World» (bit.ly/vVbVHC) или более трудного в понимании проекта «Hilo» для JavaScript (bit.ly/SgI0AA). Если вы не читали предыдущую статью, найдите ее по ссылке msdn.microsoft.com/magazine/dn519922.

Подготовка примера

На протяжении этой статьи я буду опираться на специфические примеры, которые вы сможете опробовать в своем коде. Вы можете следовать моим примерам и просто скачать полный исходный код.

Я использую тестовые сценарии, отличающиеся от тех, которые были в предыдущей статье, поэтому вам понадобится добавить некоторые новые кнопки к глобальному NavBar, если вы предпочли следовать моим примерам. (Или, если хотите, можно просто начать совершенно новый проект приложения Navigation — это тоже подойдет.) Новые NavBarCommand показан на рис. 1.

Рис. 1. Дополнительные NavBarCommand в Default.html

<div data-win-control="WinJS.UI.NavBar">
  <div data-win-control="WinJS.UI.NavBarContainer">
    <!-- Другие элементы NavBarCommand -->
    <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>

В этих тестовых сценариях я использую более реалистичную ситуацию с приложением, которое извлекает контент из Web. Это приложение получает данные от веб-сервиса Print & Photographs Online Catalog Библиотеки Конгресса США (1.usa.gov/1d8nEio). Я написал модуль, который обертывает вызовы этого веб-сервиса в объекты обещания (promise objects) и определяет классы для хранения полученных данных. Этот модуль (он находится в файле /js/searchLOC.js) показан на рис. 2.

Рис. 2. Доступ к веб-сервису Print & Photographs Online Catalog

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

Не забудьте добавить ссылку (link) на файл searchLOC.js из default.html в корне вашего проекта, прежде чем пытаться вызывать функции из него.

Удаление объектов

В JavaScript объект остается в памяти до тех пор, пока он достижим по лексической среде или по цепочке ссылок. Как только все ссылки на объект удаляются, сборщик мусора (Garbage Collector, GC) отбирает память у объекта. Пока есть хоть одна ссылка на объект, этот объект будет находиться в памяти. Утечка памяти происходит, если ссылка на объект (а значит, и сам объект) остается дольше, чем это необходимо.

Одна из распространенных причин утечек памяти в приложениях на JavaScript связана с «зомби»-объектами, которые обычно появляются, когда JavaScript-объект ссылается на какой-то DOM-объект, а этот DOM-объект удаляется из документа (вызовом removeChild или innerHTML). И JavaScript-объект остается в памяти, хотя соответствующий HTML уже исчез:

var newSpan = document.createElement("span");
document.getElementById("someDiv").appendChild(newSpan);
document.getElementById("someDiv").innerHTML = "";
WinJS.log && WinJS.log(newSpan === "undefined");
// Предыдущее выражение выводит false в JavaScript-консоль.
// Переменная newSpan остается, хотя соответствующий
// DOM-объект уничтожен.

В обычной веб-странице объект живет ровно столько, сколько эта страница отображается в браузере. В приложениях Windows Store нельзя игнорировать такие виды утечек памяти. В приложениях обычно используется одна HTML-страница как хост контента, и эта страница сохраняется на всем протяжении сеанса работы с приложением (который может длиться днями или даже месяцами). Если приложение изменяет состояние (например, пользователь переходит с одной страницы на другую или содержимое элемента управления ListView прокручивается так, что некоторые элементы уходят из поля зрения) без очистки памяти, выделенной больше не нужным JavaScript-объектам, эта память может стать недоступной приложению.

Проверка на утечки памяти

К счастью, в Visual Studio 2013 есть новые средства, способные помочь разработчикам в локализации причин утечек памяти, в частности окно Performance and Diagnostics. Для этого и следующего тестовых сценариев я продемонстрирую пару таких инструментов.

В первом тестовом сценарии я добавлю в решение пользовательский элемент управления, в котором намеренно допущена утечка памяти. Этот элемент управления с именем SearchLOCControl (/js/SearchLOCControl.js) создает текстовое поле поиска, а затем отображает результаты — после приема ответа на запрос. Код для SearchLOCControl.js показан на рис. 3. И вновь не забудьте о ссылке на этот новый JavaScript-файл из default.html.

Рис. 3. Пользовательский SearchLOCControl

(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>";
   // Примечание: это необычный метод выполнения данной задачи.
   // Код здесь максимально сокращен.    
      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);
             });
           }
         }
      })
   })
})();

Заметьте, что для создания своего элемента управления я использую jQuery и добавляю его в решение с помощью NuGet Package Manager. Загрузив этот NuGet-пакет в решение, вы должны вручную добавить ссылку на библиотеку jQuery в default.html.

SearchLOCControl полагается на некоторые стили, которые я добавил в default.css (/css/default.css); содержимое этого файла показано на рис. 4.

Рис. 4. Стили, добавленные в 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;
  }

Теперь я добавляю в решение элемент управления страницы с именем dispose.html (/pages/dispose/dispose.html) и вставляю следующую HTML-разметку в тег <section> для dispose, чтобы создать пользовательский элемент управления:

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

Наконец, я добавляю код в обработчик событий PageControl.ready в файл dispose.js (/pages/dispose/dispose.js), который наивно уничтожает элемент управления и создает утечку памяти, присваивая innerHTML основного <div> этого элемента управления пустую строку (рис. 5).

Рис. 5. Код в dispose.js для «уничтожения» пользовательского элемента управления

(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 = "";
      });
    }
  // Прочий код элемента управления страницы...
  });
})();

Теперь я могу проверить использование памяти элементом управления. Окно Performance and Diagnostics предоставляет несколько инструментов для измерения производительности приложения Windows Store, в том числе нагрузки на процессор, энергопотребления приложения, «отзывчивости» UI и времени выполнения JavaScript-функций. (Подробнее об этих инструментах см. в блоге группы Visual Studio по ссылке bit.ly/1bESdOH.) Если этого окна еще нет, откройте панель Performance and Diagnostics либо через меню Debug (Visual Studio Express 2013 for Windows), либо через меню Analyze (Visual Studio Professional 2013 и Visual Studio Ultimate 2013).

Для этого теста я использую средство мониторинга памяти, выделяемой JavaScript-коду. Ниже перечислены этапы выполнения этого теста.

  1. В окне Performance and Diagnostics выберите JavaScript Memory и щелкните Start. Проект будет запущен в режиме отладки. Если появится диалоговое окно User Account Control, щелкните Yes.
  2. При запущенном проекте приложения перейдите на страницу dispose, а затем переключитесь на рабочий стол. В Visual Studio в текущем диагностическом сеансе (вкладка Report*.diagsession) щелкните Take Heap Snapshot.
  3. Переключитесь обратно в выполняемое приложение. Введите в поле поиска запрос (например, «Lincoln») и нажмите Enter. Появится элемент управления ListView, который отображает результаты поиска изображений.
  4. Вновь переключитесь на рабочий стол. В Visual Studio в текущем диагностическом сеансе (вкладка Report*.diagsession) щелкните Take Heap Snapshot.
  5. Вернитесь в выполняемое приложение. Щелкните кнопку Dispose. Пользовательский элемент управления исчезнет со страницы.
  6. Переключитесь на рабочий стол. В Visual Studio в текущем диагностическом сеансе (вкладка Report*.diagsession) щелкните Take Heap Snapshot, а затем выберите Stop. Теперь в диагностическом сеансе у вас есть список из трех снимков, как показано на рис. 6.

Использование памяти до реализации шаблона dispose
Рис. 6. Использование памяти до реализации шаблона dispose

Располагая диагностическими данными, можно проанализировать использование памяти пользовательским элементом управления. Взглянув на результаты диагностического сеанса я подозреваю, что «удаление» элемента управления не привело к освобождению всей связанной с ним памяти.

В отчете можно изучить JavaScript-объекты в куче для каждого снимка. Я хочу узнать, что осталось в памяти после удаления пользовательского элемента управления из DOM. Я щелкаю ссылку, связанную с количеством объектов в куче в третьем снимке (Snapshot #3 на рис. 6).

Сначала я изучу представление Dominators, которое показывает список объектов, отсортированных по размеру занимаемой памяти. Объекты, использующие наибольшие объемы памяти (потенциально их легче всего освободить), находятся в верхней части списка. В представлении Dominators я вижу ссылку на <div> с id, значение которого равно «searchControl». Раскрыв ее, я обнаруживаю, что поле поиска, ListView и сопоставленные с ним данные — все они находятся в памяти.

Щелкнув правой кнопкой мыши строку с <div> для searchControl и выбрав Show в представлении Roots, можно увидеть, что обработчики событий для щелчков кнопок тоже присутствуют в памяти (рис. 7).

Код неподключенного обработчика событий впустую занимает память
Рис. 7. Код неподключенного обработчика событий впустую занимает память

К счастью, это легко исправить, внеся в код всего несколько изменений.

Реализация шаблона dispose в WinJS

В WinJS 2.0 все WinJS-элементы управления реализуют шаблон dispose, чтобы избежать утечек памяти. Всякий раз, когда WinJS-элемент управления выходит из области видимости (например, когда пользователь переходит на другую страницу), WinJS удаляет все ссылки на него. Этот элемент помечается к удалению, что сообщает сборщику мусора освободить всю память, выделенную этому объекту.

У шаблона dispose в WinJS есть три важных характеристики, которые должен реализовать любой элемент управления для корректного удаления:

  • DOM-элемент контейнера верхнего уровня должен иметь CSS-класс win-disposable;
  • класс элемента управления должен включать поле _disposed, которое изначально равно false. Вы можете добавить этот член к элементу управления (наряду с CSS-классом win-disposable) вызовом WinJS.Utilities.markDisposable;
  • JavaScript-класс, определяющий элемент управления, должен предоставлять метод dispose. В методе dispose:
    • вся память, выделенная объектам, связанным с этим элементом управления, должна освобождаться;
    • все обработчики событий должны отключаться от дочерних DOM-объектов;
    • у всех дочерних объектов элемента управления должны вызываться их методы dispose. Лучше всего делать это вызовом WinJS.Utilities.disposeSubTree в хост-элементе;
    • все незавершенные обещания, на которые могут быть ссылки внутри элемента управления, должны быть отменены (вызовом метода Promise.cancel с последующим обнулением переменной).

Поэтому в функцию-конструктор для SearchLOCControl.Control я добавляю следующие строки кода:

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

Затем в определение класса SearchLOCControl (вызов WinJS.Class.define) я включаю новый член экземпляра с именем dispose. Вот как выглядит код для метода 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;
}

В принципе, вы не обязаны очищать каждую переменную в своем коде, который полностью заключен в код для элемента. Когда код для элемента управления выходит из области видимости, то же самое происходит и со всеми внутренними переменными. Однако, если этот код ссылается на нечто внешнее, например на DOM-элемент, то эту ссылку нужно обнулить.

Наконец, я добавляю явный вызов метода dispose в dispose.js (/pages/dispose/dispose.js). Ниже показан обновленный обработчик события щелчка для кнопки в dispose.html:

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

Теперь, когда я запускаю тот же тест JavaScript-памяти, результаты диагностического сеанса выглядят гораздо лучше (рис. 8).

Использование памяти после реализации dispose
Рис. 8. Использование памяти после реализации dispose

Анализируя память в куче, я вижу, что у <div> для searchControl больше нет связанных с ним дочерних элементов (рис. 9). Ни один из вложенных элементов управления не остается в памяти, а также удаляются все связанные с ними обработчики событий (рис. 10).

Представление Dominators после реализации dispose
Рис. 9. Представление Dominators после реализации dispose

Представление Roots после реализации dispose
Рис. 10. Представление Roots после реализации dispose

Улучшение «отзывчивости»: планировщик и Web Workers

Приложения могут перестать отвечать, когда UI ожидает обновления на основе внешнего процесса. Например, если приложение выдает несколько запросов какому-то веб-сервису, чтобы заполнить некий UI-элемент, этот элемент и весь UI могут «зависнуть» в ожидании ответов на запросы. А это приведет к тому, что приложение покажется зависшим.

Чтобы продемонстрировать это, я создал другой тестовый сценарий, где заполняю элемент управления Hub «избранными коллекциями» («featured collections»), предоставляемыми веб-сервисом Библиотеки Конгресса. Я добавляю в проект новый Page Control с именем scheduler.html для тестового сценария (/pages/scheduler/scheduler.js). В HTML для этой страницы я объявляю элемент управления Hub, содержащий шесть элементов управления HubSection (по одному на каждую избранную коллекцию). HTML для элемента управления Hub в тегах <section> в scheduler.html показан на рис. 11.

Рис. 11. Элементы управления Hub и HubSection, объявленные в 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>

Затем я получаю данные для избранных коллекций от веб-сервиса. Я добавлю в решение новый файл с именем data.js (/js/data.js), который вызывает веб-сервис и возвращает объект WinJS.Binding.List. На рис. 12 показан код для получения данных избранных коллекций. И вновь не забудьте о ссылке на data.js из default.html.

Рис. 12. Получение данных от веб-сервиса

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

Теперь мне нужно вставить данные в элемент управления Hub. В файл scheduler.js (/pages/scheduler/scheduler.js) я добавлю код в функцию PageControl.ready и определю новую функцию, populateSection. Полный код приведен на рис. 13.

Рис. 13. Динамическое заполнение элемента управления Hub

(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();
    }
    // Прочие члены PageControl...
  });
  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/>")
    }
    }
})();

Заметьте, что на рис. 13 я получаю ссылку на обещание, возвращаемое вызовом Data.getFeaturedCollections, а затем явным образом отменяю обещание, когда страница выгружается. Это предотвращает возможные условия гонок в сценарии, где пользователь переходит на страницу, а потом уходит с нее до возврата управления вызовом getFeaturedCollections.

Нажав F5 и перейдя в scheduler.html, я замечаю, что после загрузки страницы элемент управления Hub заполняется медленно. На моем компьютере это может просто раздражать, а на менее мощных машинах задержка может оказаться слишком большой.

Visual Studio 2013 включает средства для измерения «отзывчивости» UI в приложении Windows Store. В панели Performance and Diagnostics я выбираю тест HTML UI Responsiveness и щелкаю Start. После запуска приложения я перехожу в scheduler.html и наблюдаю за результатами, которые появляются в элементе управления Hub. По окончании этой задачи я переключаюсь на Desktop и щелкаю Stop на вкладке диагностического сеанса. Результаты показаны на рис. 14.

HTML UI Responsiveness для Scheduler.html
Рис. 14. HTML UI Responsiveness для Scheduler.html

Я вижу, что примерно на полсекунды частота кадров упала до 3 FPS (кадров в секунду). Я выбираю период малой частоты кадров, чтобы увидеть подробности (рис. 15).

Подробная хронология периода, в течение которого UI-поток оценивает scheduler.js
Рис. 15. Подробная хронология периода, в течение которого UI-поток оценивает scheduler.js

В этой точке хронологии (рис. 15) UI-поток полностью занят выполнением scheduler.js. Если вы внимательнее изучите детали хронологии, то заметите несколько оранжевых меток. Они указывают на специфические вызовы performance.mark в коде. В scheduler.js первый вызов performance.mark происходит, когда scheduler.html загружается. Заполнение контентом каждого элемента управления HubSection инициирует следующий вызов. По этим результатам более половины времени, в течение которого выполнялась оценка scheduler.js, приходится на интервал между моментом, когда я перешел на страницу (первая метка), и моментом, когда изображениями был заполнен шестой HubSection (последняя метка).

(Учитывайте, что результаты могут варьироваться в зависимости от вашего аппаратного обеспечения. Тесты HTML UI Responsiveness, показанные в этой статье, выполнялись на Microsoft Surface Pro с процессором третьего поколения Intel Core i5-3317U, работающим на частоте 1,7 ГГц, и видеокартой Intel HD Graphics 400.)

Чтобы уменьшить задержку, нужно переработать код так, чтобы элементы управления HubSection заполнялись по скользящему графику. Пользователи видят контент в приложении вскоре после перехода в него. Контент для первых двух HubSection следует загружать немедленно после перехода, а остальные HubSection можно загружать позже.

Планировщик

JavaScript — однопоточная среда, а значит, все происходит в UI-потоке. В WinJS 2.0 введен WinJS.Utilities.Scheduler для организации работы, выполняемой в UI-потоке (детали см. по ссылке bit.ly/1bFbpfb).

Планировщик (Scheduler) создает единственную очередь заданий, которые должны быть выполнены UI-потоком в приложении. Задания (jobs) выполняются на основе приоритета, при этом задания с более высоким приоритетом могут вытеснять или откладывать выполнение заданий с более низким приоритетом. Задания планируются в зависимости от операций пользователя в UI-потоке, где Scheduler распределяет процессорное время между вызовами и по возможности стремится выполнять как можно больше заданий из очереди.

Как упоминалось, планировщик выполняет задания на основе их приоритета, задаваемого с помощью перечисления WinJS.Utilities.Scheduler.Priority. В этом перечислении семь значений (в порядке убывания): max, high, aboveNormal, normal, belowNormal, idle и min. Задания с равным приоритетом выполняются по принципу «первым вошел — первым вышел».

Но вернемся к тестовому сценарию. Я создаю задание в Scheduler, чтобы заполнить каждый HubSection при загрузке scheduler.html. Для каждого HubSection я вызываю Scheduler.schedule и передаю функцию, которая заполняет HubSection. Первые два задания запускаются с обычным приоритетом (normal), а остальные выполняются, только когда UI-поток простаивает. В третьем параметре (thisArg) для метода schedule я передаю некий контекст для задания.

Метод schedule возвращает объект Job, который позволяет мне отслеживать прогресс в выполнении задания или отменять его. Свойству owner каждого задания я назначаю один и тот же объект OwnerToken. Это позволяет отменить все запланированные задания с атрибутом в виде такого маркера владельца. Детали см. на рис. 16.

Рис. 16. Обновленный scheduler.js, использующий Scheduler API

(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();
    }
  // Прочие члены PageControl...
  });
  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/>")
    }
  }
})();

Теперь, когда я запускаю диагностический тест HTML UI Responsiveness, я должен увидеть несколько иные результаты. Результаты второго теста представлены на рис. 17.

HTML UI Responsiveness после использования scheduler.js
Рис. 17. HTML UI Responsiveness после использования scheduler.js

Во время второго теста частота кадров, визуализируемых приложением, упала на более короткий период. Также стало удобнее использовать приложение: элемент управления Hub быстрее заполняется, и задержки почти нет.

Потоки Web Worker

Стандартная веб-платформа включает Web Worker API, позволяющий приложению выполнять фоновые задачи вне UI-потока. Если в двух словах, то Web Worker (или просто Worker) вводит многопоточность в приложения на JavaScript. Вы передаете простые сообщения (строку или простой JavaScript-объект) потоку Worker, а тот возвращает ответные сообщения основному потоку через метод postMessage.

Потоки Worker выполняются в скриптовом контексте, отличным от остального приложения, поэтому они не могут обращаться к UI. Вы не можете создавать новые HTML-элементы с помощью createElement или использовать функционал сторонних библиотек, которые полагаются на объект документа (например, jQuery-функцию $). Однако потоки Worker могут обращаться к Windows Runtime API, а значит, они могут записывать данные приложения, генерировать всплывающие уведомления (toasts), вызывать обновления плиток или даже сохранять файлы. Они отлично подходят для фоновых задач, не требующих ввода от пользователя, затратных с точки зрения вычислительных ресурсов или отправляющих несколько вызовов какому-либо веб-сервису. За более подробной информацией о Web Worker API обращайтесь к справочной документации по Worker (bit.ly/1fllmip).

Манипуляции с DOM влияют на «отзывчивость» UI

Добавление новых элементов в DOM в HTML-странице может ухудшить производительность, особенно если таких элементов достаточно много. Потребуется пересчитывать позиции остальных элементов на странице, заново применять стили и, наконец, перерисовывать страницу. Например, CSS-инструкция, которая задает верхнюю и левую координаты, ширину, высоту или стиль отображения элемента, вызовет все эти операции в странице. (Советую вместо этого использовать либо встроенные средства анимации в WinJS, либо преобразования анимации, доступные в CSS3, для манипуляций позициями HTML-элементов.)

Тем не менее, встраивание и отображение динамического контента — распространенная практика. Лучший вариант для максимальной производительности — по возможности использовать связывание с данными, поддерживаемое платформой. Связывание с данными в WinJS оптимизировано так, чтобы вы могли создать быстро работающий и отзывчивый UI.

В ином случае вам понадобится выбирать между встраиванием HTML как строки в другой элемент с помощью innerHTML и добавлением индивидуальных элементов по одному за раз, используя createElement и appendChild. Применение innerHTML, как правило, обеспечивает более высокую производительность, но вы можете оказаться не в состоянии манипулировать HTML после его вставки.

В своих примерах я предпочел метод $.append в jQuery. С помощью append я могу передавать HTML как строку и немедленно получать программный доступ к новым DOM-узлам. (Кроме того, он обеспечивает вполне приличную производительность.)

Преимущество использования потока Worker в том, что фоновая работа никак не влияет на отзывчивость UI. UI продолжает реагировать на действия пользователя, и выпадения кадров практически не наблюдается. Кроме того, потоки Worker могут импортировать другие JavaScript-библиотеки, которые не опираются на DOM, в том числе фундаментальную библиотеку для WinJS (base.js). Поэтому вы можете, например, создавать обещания в потоке Worker.

С другой стороны, потоки Worker не являются панацеей ото всех проблем с производительностью. Процессорное время для потоков Worker все равно выделяется из общего процессорного времени, доступного на компьютере, даже если оно не отбирается у UI-потока. Вам нужно проявлять благоразумие в использовании потоков Worker.

Для следующего тестового сценария я задействую поток Worker, чтобы получать набор изображений из Библиотеки Конгресса и заполнять этими картинками элемент управления ListView. Сначала я добавлю новый скрипт для сохранения потока Worker с именем LOC-worker.js в своем проекте:

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

С помощью функции importScripts я включаю base.js из библиотеки WinJS и скрипты seachLOC.js в контекст Worker, делая их доступными для использования.

Затем я добавляю в проект новый Page Control с именем worker.html (/pages/worker/worker.html). В теги <section> в worker.html я вставляю кое-какую разметку для размещения элемента управления ListView и определения его структуры. Этот элемент управления будет создаваться динамически при возврате управления из Worker:

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

Наконец, я добавляю код в worker.js, чтобы создать новый поток Worker, а затем заполнить HTML на основе ответа. Код в worker.js показан на рис. 18.

Рис. 18. Создание потока Worker и последующее заполнение UI-элемента

(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);
    }
  // Прочие члены PageControl...
  });
  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}
      });
  }
})();

Запустив приложение и перейдя на эту страницу, вы заметите минимальную задержку между моментом перехода и моментом появления изображений в ListView. Если вы запустили этот тестовый сценарий с помощью инструмента HTML UI Responsiveness, то увидите вывод, похожий на тот, который показан на рис. 19.

HTML UI Responsiveness при использовании потока Worker
Рис. 19. HTML UI Responsiveness при использовании потока Worker

Обратите внимание на то, что в приложении выпадает крайне мало кадров после того, как я перехожу на страницу worker.html (после первой метки в хронологии). UI остается невероятно отзывчивым благодаря тому, что выборка данных была вынесена в поток Worker.

Когда выбирать между Scheduler и Worker API

Поскольку и Scheduler API, и Worker API позволяют управлять фоновыми задачами в вашем коде, возможно, вас интересует, когда следует использовать тот или другой API. (Пожалуйста, обратите внимание на то, что в двух примерах кода, представленных в этой статье, не ставилось задачи четкого сравнения этих двух API.)

Worker API — ввиду выполнения в другом потоке — обеспечивает более высокую производительность, чем Scheduler при прямом сравнительном исследовании. Однако, поскольку Scheduler использует часть процессорного времени UI-потока, он имеет контекст текущей страницы. Вы можете применять Scheduler для обновления UI-элементов на странице или для динамического создания новых элементов на странице.

Если вашему фоновому коду нужно так или иначе взаимодействовать с UI, вам следует выбрать Scheduler. Но, если ваш код не полагается на контекст приложения и лишь передает простые данные, подумайте об использовании Worker. Преимущество потока Worker в том, что фоновая работа не влияет на отзывчивость UI.

Scheduler API и Web Worker API вовсе не являются единственным выбором для создания нескольких потоков в приложении Windows Store. Вы также можете создать компонент Windows Runtime на C++, C# или Visual Basic .NET, способный порождать новые потоки. WinRT-компоненты способны предоставлять API, которые можно вызывать из кода на JavaScript. Подробнее на эту тему см. по ссылке bit.ly/19DfFaO.

Сомневаюсь, что разработчики хотят писать дефектные, «глючные» или неотзывчивые приложения (если только они не пишут статью о таких приложениях). Фокус в том, чтобы отыскать эти дефекты в коде и устранить их, желательно до передачи приложения пользователям. В этой серии статей я продемонстрировал несколько инструментов для локализации проблем в вашем коде и методики написания более эффективного кода.

Конечно, ни одна из этих методик или инструментов не является панацеей, автоматически устраняющей любые проблемы. Они способны помочь в улучшении приложения, но никак не исключают необходимости придерживаться принятых практик кодирования. Фундаментальные основы программирования приложений Window Store на JavaScript, в целом, остаются теми же, что и для любой другой платформы.


Эрик Шмидт (Eric Schmidt) — разработчик контента в группе Microsoft Windows Developer Content, пишет о Windows Library for JavaScript (WinJS). Ранее работал в Microsoft Office Division, где создавал примеры кода для платформы Apps for Office. Свободное время проводит с семьей, играет на контрабасе, создает видеоигры для HTML5 и ведет блог о конструкторах LEGO (historybricks.com).

Выражаю благодарность за рецензирование статьи экспертам Microsoft Крейгу Брокшмидту (Kraig Brockschmidt), Грегу Булмэшу (Greg Bulmash) и Джошу Уильямсу (Josh Williams).