Исследуя, как создавать более эффективные приложения 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-коду. Ниже перечислены этапы выполнения этого теста.
- В окне Performance and Diagnostics выберите JavaScript Memory и щелкните Start. Проект будет запущен в режиме отладки. Если появится диалоговое окно User Account Control, щелкните Yes.
- При запущенном проекте приложения перейдите на страницу dispose, а затем переключитесь на рабочий стол. В Visual Studio в текущем диагностическом сеансе (вкладка Report*.diagsession) щелкните Take Heap Snapshot.
- Переключитесь обратно в выполняемое приложение. Введите в поле поиска запрос (например, «Lincoln») и нажмите Enter. Появится элемент управления ListView, который отображает результаты поиска изображений.
- Вновь переключитесь на рабочий стол. В Visual Studio в текущем диагностическом сеансе (вкладка Report*.diagsession) щелкните Take Heap Snapshot.
- Вернитесь в выполняемое приложение. Щелкните кнопку Dispose. Пользовательский элемент управления исчезнет со страницы.
- Переключитесь на рабочий стол. В Visual Studio в текущем диагностическом сеансе (вкладка Report*.diagsession) щелкните Take Heap Snapshot, а затем выберите Stop. Теперь в диагностическом сеансе у вас есть список из трех снимков, как показано на рис. 6.
![*](/user_img/140608143900/dn574803.Schmidt_6_hires(ru-ru,MSDN.10).png)
Рис. 6. Использование памяти до реализации шаблона dispose
Располагая диагностическими данными, можно проанализировать использование памяти пользовательским элементом управления. Взглянув на результаты диагностического сеанса я подозреваю, что «удаление» элемента управления не привело к освобождению всей связанной с ним памяти.
В отчете можно изучить JavaScript-объекты в куче для каждого снимка. Я хочу узнать, что осталось в памяти после удаления пользовательского элемента управления из DOM. Я щелкаю ссылку, связанную с количеством объектов в куче в третьем снимке (Snapshot #3 на рис. 6).
Сначала я изучу представление Dominators, которое показывает список объектов, отсортированных по размеру занимаемой памяти. Объекты, использующие наибольшие объемы памяти (потенциально их легче всего освободить), находятся в верхней части списка. В представлении Dominators я вижу ссылку на <div> с id, значение которого равно «searchControl». Раскрыв ее, я обнаруживаю, что поле поиска, ListView и сопоставленные с ним данные — все они находятся в памяти.
Щелкнув правой кнопкой мыши строку с <div> для searchControl и выбрав Show в представлении Roots, можно увидеть, что обработчики событий для щелчков кнопок тоже присутствуют в памяти (рис. 7).
![*](/user_img/140608143900/dn574803.Schmidt_8_hires(ru-ru,MSDN.10).png)
Рис. 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).
![*](/user_img/140608143900/dn574803.Schmidt_9_hires(ru-ru,MSDN.10).png)
Рис. 8. Использование памяти после реализации dispose
Анализируя память в куче, я вижу, что у <div> для searchControl больше нет связанных с ним дочерних элементов (рис. 9). Ни один из вложенных элементов управления не остается в памяти, а также удаляются все связанные с ними обработчики событий (рис. 10).
![*](/user_img/140608143900/dn574803.Schmidt_10_hires(ru-ru,MSDN.10).png)
Рис. 9. Представление Dominators после реализации dispose
![*](/user_img/140608143900/dn574803.Schmidt_11_hires(ru-ru,MSDN.10).png)
Рис. 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.
![*](/user_img/140608143900/dn574803.Schmidt_15_hires(ru-ru,MSDN.10).png)
Рис. 14. HTML UI Responsiveness для Scheduler.html
Я вижу, что примерно на полсекунды частота кадров упала до 3 FPS (кадров в секунду). Я выбираю период малой частоты кадров, чтобы увидеть подробности (рис. 15).
![*](/user_img/140608143900/dn574803.Schmidt_16_hires(ru-ru,MSDN.10).png)
Рис. 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.
![*](/user_img/140608143900/dn574803.Schmidt_18_hires(ru-ru,MSDN.10).png)
Рис. 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.
![*](/user_img/140608143900/dn574803.Schmidt_20_hires(ru-ru,MSDN.10).png)
Рис. 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, в целом, остаются теми же, что и для любой другой платформы.