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

OSzone.net » Microsoft » Разработка приложений » Windows (до Windows 10) » Создание более эффективных приложений Windows Store с применением JavaScript: производительность
Автор: Эрик Шмидт
Иcточник: MSDN
Опубликована: 19.06.2014

Исследуя, как создавать более эффективные приложения 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.

*
Рис. 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 есть три важных характеристики, которые должен реализовать любой элемент управления для корректного удаления:

Поэтому в функцию-конструктор для 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).

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

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

*
Рис. 9. Представление Dominators после реализации 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.

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

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

*
Рис. 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.

*
Рис. 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.

*
Рис. 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, в целом, остаются теми же, что и для любой другой платформы.


Ссылка: http://www.oszone.net/24355/JavaScript