Оптимизация отрисовки элементов ListView

Для многих приложений Магазина Windows, написанных на JavaScript и работающих с коллекциями, хорошее взаимодействие с элементом управления WinJS ListView просто необходимо для оптимизации производительности. И это неудивительно: когда вы занимаетесь отображением тысяч элементов и их управлением, ценится любое усилие по их оптимизации. Самое важное — как каждый из этих элементов отрисовывается, т.е. как и когда каждый элемент из ListView формируется в DOM и становится видимой частью приложения. Время (компонент "когда") в этом уравнении становится критически важным фактором, если пользователь быстро перемещается по списку и ожидает, что список будет "успевать" за его перемещениями.

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

  • Асинхронная доставка данных элемента и отображаемого элемента с поддержкой базовых функций отрисовки.
  • Раздельное создание формы элементы, необходимой для общей разметки ListView, и его внутренних компонентов. Это поддерживается с помощью средства отрисовки заполнителя.
  • Повторное использование ранее созданного элемента (и дочерних элементов) с заменой данных, что позволяет пропустить большинство действий по созданию элемента. Предоставляется с помощью средства отрисовки заполнителя с повторным использованием.
  • Задержка выполнения ресурсоемких визуальных операций, таких как загрузка изображений и анимация, до отображения элемента (если для ListView не используется быстрое панорамирование) с помощью средства многоэтапной отрисовки.
  • Пакетное выполнение одинаковых визуальных операций, чтобы свести к минимуму повторную отрисовку DOM, с помощью средства многоэтапной пакетной отрисовки.

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

Для всех средств отрисовки всегда важно свести к минимуму базовое время отрисовки элемента (не учитывая отложенные операции). Поскольку конечная производительность ListView сильно зависит от того, насколько его обновления согласованы с интервалами обновления экрана, несколько лишних миллисекунд в отрисовке элемента могут значительно увеличить общее время визуализации ListView в течение следующего интервала обновления. Из-за этого могут пропадать кадры, а изображение будет казаться нестабильным. Другими словами, если вы хотите оптимизировать код JavaScript, делайте это в средствах отрисовки элементов.

Базовые средства отрисовки

Начнем с краткого обзора функции отрисовки элемента, которую я буду называть средством отрисовки. Средство отрисовки — это функция, которую вы назначаете свойству itemTemplate элемента управления ListView вместо имени шаблона. Она при необходимости вызывается для элементов, которые ListView хочет включить в DOM. (Кстати, базовую документацию по средствам отрисовки можно найти на странице itemTemplate. Но описанные возможности оптимизации демонстрируются именно в примере.)

Можно ожидать, что функция отрисовки просто получает элемент из источника данных ListView. Затем она создает необходимые HTML-элементы и возвращает корневой элемент, который ListView может добавить в DOM. В целом, именно эти и происходит, но стоит отметить два момента. Во-первых, сами данные элемента могут загружаться асинхронно, поэтому имеет смысл привязать создание элемента к доступности данных. Кроме того, в процесс отрисовки элемента могут входить другие асинхронные операции, например загрузка изображений из удаленных URI или чтение данных из других файлов, определенных в данных элемента. Как мы увидим, разные уровни оптимизации позволяют выполнять произвольный объем асинхронных операций между запросом компонентов элемента и их фактической доставкой.

Опять же, можно ожидать использования обещаний! ListView не просто напрямую передает данные элемента средству отрисовки, а предоставляет обещание об этих данных. А функция не возвращает корневой компонент элемента напрямую, а возвращает обещание об этом компоненте. Это позволяет ListView объединить множество обещаний отрисовки элемента и ждать (асинхронно) визуализации целой страницы. Так ListView будет интеллектуально управлять формированием разных страниц, сначала создавая страницу видимых элементов, а затем — предыдущую и следующую страницы, на которые вероятнее всего перейдут пользователи. Кроме того, наличие всех этих обещаний означает, что ListView может легко отменить отрисовку незаконченных элементов, если пользователь перейдет к другим элементам. Это позволит избежать создания ненужных элементов.

Использование таких обещаний можно увидеть в функции simpleRenderer в примере:

 

 function simpleRenderer(itemPromise) {
    return itemPromise.then(function (item) {
        var element = document.createElement("div");
        element.className = "itemTempl";
        element.innerHTML = "<img src='" + item.data.thumbnail +
            "' alt='Databound image' /><div class='content'>" + item.data.title + "</div>";
        return element;
    });
}

Этот код сначала присоединяет обработчик выполнения к itemPromise. Этот обработчик вызывается, когда данные элемента доступны, и создает в ответ компоненты. Но снова обратите внимание, что мы не возвращаем компонент напрямую, а возвращаем обещание, которое будет выполнено с этим компонентом. Следовательно возвращаемое itemPromise.then() значение — это обещание, выполняемое с компонентом, когда и если он потребуется ListView.

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

 function someRenderer(itemPromise) {
    return itemPromise.then(function (item) {
        return doSomeWorkAsync(item.data);
    }).then(function (results) {
        return doMoreWorkAsync(results1);
    }).then(function (results2) {
        var element = document.createElement("div");
        // use results2 to configure the element
        return element;
    });
}

Обратите внимание, что в этом случае мы не используем функцию done в конце цепочки, поскольку мы возвращаем обещание от последнего вызова функции then. ListView отвечает за обработку всех ошибок, которые могут возникнуть.

Средства отрисовки заполнителя

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

Средство отрисовки заполнителя возвращает объект с двумя свойствами, а не обещание:

  • element — компонент верхнего уровня в структуре отображаемого элемента. Его достаточно для определения размера и формы, и он не зависит от данных элемента.
  • renderComplete — обещание, которое выполняется после формирования оставшихся частей компонента, т. е. когда возвращается обещание из цепочки, начинающейся с itemPromise.then, как и ранее.

ListView реализован достаточно интеллектуально, поэтому он проверяет, возвращает ли средство отрисовки обещание (базовая ситуация, как описано ранее) или объект со свойствами element и renderComplete (более сложные ситуации). Таким образом, эквивалентное средство отрисовки заполнителя (в примере) для предыдущей функции simpleRenderer выглядит следующим образом:

 function placeholderRenderer(itemPromise) {
    // create a basic template for the item that doesn't depend on the data
    var element = document.createElement("div");
    element.className = "itemTempl";
    element.innerHTML = "<div class='content'>...</div>";

    // return the element as the placeholder, and a callback to update it when data is available
    return {
        element: element,

        // specifies a promise that will be completed when rendering is complete
        // itemPromise will complete when the data is available
        renderComplete: itemPromise.then(function (item) {
            // mutate the element to include the data
            element.querySelector(".content").innerText = item.data.title;
            element.insertAdjacentHTML("afterBegin", "<img src='" +
                item.data.thumbnail + "' alt='Databound image' />");
        })
    };
}

Обратите внимание на то, что назначение element.innerHTML можно было бы переместить в renderComplete, поскольку класс itemTempl в файле css/scenario1.css примера напрямую определяет ширину и высоту элемента. Это назначение указано в свойстве element, потому что оно задает в заполнителе текст по умолчанию "…". Вы можете так же просто использовать компонент img, который ссылается на небольшой ресурс в пакете, общий для всех элементов (и поэтому быстро визуализируемый).

Средства отрисовки заполнителя с повторным использованием

Следующая возможность оптимизации — применение средства отрисовки заполнителя с повторным использованием — не добавляет ничего нового в отношении обещаний. Она добавляет знание о втором параметре средства отрисовки, recycled, который является корневым компонентом ранее отрисованного элемента, который больше не виден. Это значит, что дочерние компоненты повторно используемого компонента уже сформированы, поэтому можно просто заменить данные и, возможно, подправить несколько компонентов. Так можно избежать ресурсоемких вызовов создания компонентов, необходимых для отображения совершенно нового элемента. Это позволяет сэкономить много времени в процессе отрисовки.

ListView может предоставить повторно используемый компонент, если для его свойства loadingBehavior задано значение "randomaccess". Если указан параметр recycled, можно просто удалить данные этого компонента (и его дочерних компонентов), вернуть его как заполнитель, а затем вставить в него данные и создать дополнительные дочерние компоненты (при необходимости) в renderComplete. Если повторно используемый компонент не указан (как при первом создании ListView или если для параметра loadingBehavior задано значение "incremental"), компонент создается заново. Вот код из примера для этого варианта:

 function recyclingPlaceholderRenderer(itemPromise, recycled) {
    var element, img, label;
    if (!recycled) {
        // create a basic template for the item that doesn't depend on the data
        element = document.createElement("div");
        element.className = "itemTempl";
        element.innerHTML = "<img alt='Databound image' style='visibility:hidden;'/>" +
            "<div class='content'>...</div>";
    }
    else {
        // clean up the recycled element so that we can reuse it 
        element = recycled;
        label = element.querySelector(".content");
        label.innerHTML = "...";
        img = element.querySelector("img");
        img.style.visibility = "hidden";
    }
    return {
        element: element,
        renderComplete: itemPromise.then(function (item) {
            // mutate the element to include the data
            if (!label) {
                label = element.querySelector(".content");
                img = element.querySelector("img");
            }
            label.innerText = item.data.title;
            img.src = item.data.thumbnail;
            img.style.visibility = "visible";
        })
    };
}

В renderComplete необходимо проверить наличие компонентов, которые не создаются для нового заполнителя, например label, и создать их при необходимости.

Если вы хотите очистить повторно используемые элементы с помощью более общего процесса, можно создать функцию для свойства resetItem элемента управления ListView. Эта функция будет содержать код, аналогичный приведенному выше. То же относится и к свойству resetGroupHeader, поскольку вы можете использовать функции шаблона для заголовков групп и элементов. Мы не обсуждали их подробно, поскольку заголовков групп не так много и обычно они не так сильно влияют на производительность. Но тем не менее такая возможность существует.

Средства многоэтапной отрисовки

Теперь перейдем к предпоследней возможности оптимизации — средству многоэтапной отрисовки. Оно расширяет возможности средства отрисовки заполнителя с повторным использованием, задерживая загрузку изображений и других объектов мультимедиа, пока весь элемент не появится в DOM. Оно также задерживает эффекты, например анимацию, пока элемент не появится на экране. Это связано с тем, что зачастую пользователи довольно быстро перемещаются по элементу управления ListView, поэтому имеет смысл асинхронно отложить более ресурсоемкие операции, пока ListView не окажется в стабильном положении.

ListView предоставляет необходимые обработчики как члены item, полученного от itemPromise: свойство ready (обещание) и два метода, loadImage и isOnScreen, также возвращающие обещания. Т. е.:

 renderComplete: itemPromise.then(function (item) {
    // item.ready, item.loadImage, and item.isOnScreen available
})

Вот как они используются:

  • ready — возвращайте это обещание из первого обработчика выполнения в цепочке. Это обещание выполняется, когда полная структура элемента отрисована и видима. Это значит, что другой метод then можно объединить с обработчиком выполнения, в котором выполняются другие операции, такие как загрузка изображений.
  • loadImage — загружает изображение по URI и отображает его в указанном компоненте img, возвращая обещание, которое выполняется с тем же элементом. Обработчик выполнения присоединяется к этому обещанию, которое само возвращает обещание от isOnScreen. Обратите внимание, что loadImage создает компонент img, если он не указан, и предоставляет его обработчику выполнения.
  • isOnScreen — возвращает обещание с логическим значением выполнения, указывающим, видим элемент или нет. В текущих реализациях это известное значение, поэтому обещание выполняется синхронно. Однако, заключив в обещание, его можно использовать в более длинной цепочке.

Все это видно в функции multistageRenderer в примере, где завершение загрузки изображения используется для начала анимации исчезания. Здесь я просто покажу, что возвращает обещание renderComplete:

 renderComplete: itemPromise.then(function (item) {
    // mutate the element to update only the title
    if (!label) { label = element.querySelector(".content"); }
    label.innerText = item.data.title;

    // use the item.ready promise to delay the more expensive work
    return item.ready;
    // use the ability to chain promises, to enable work to be cancelled
}).then(function (item) {
    // use the image loader to queue the loading of the image
    if (!img) { img = element.querySelector("img"); }
    return item.loadImage(item.data.thumbnail, img).then(function () {
        // once loaded check if the item is visible
        return item.isOnScreen();
    });
}).then(function (onscreen) {
    if (!onscreen) {
        // if the item is not visible, don't animate its opacity
        img.style.opacity = 1;
    } else {
        // if the item is visible, animate the opacity of the image
        WinJS.UI.Animation.fadeIn(img);
    }
})

Хотя и происходит много всего, здесь используется всего лишь базовая цепочка обещаний. Первая асинхронная операция в средстве отрисовки обновляет простые части структуры компонентов элемента, такие как текст. Затем она возвращает обещание в item.ready. Когда это обещание выполняется или, говоря точнее, если оно выполняется, мы используем асинхронный метод loadImage элемента, чтобы инициировать загрузку изображения. Обработчик выполнения возвращает обещание item.isOnScreen. Это значит, что флаг видимости onscreen передается в конечный обработчик выполнения в цепочке. Если и когда обещание isOnScreen выполняется (элемент полностью видим), мы можем заняться соответствующими операциями, например анимацией.

Я подчеркиваю часть с "если", так как есть вероятность, что пользователь быстро перемещается по элементу управления ListView, когда все это происходит. Объединив все обещания в цепочку, мы позволяем ListView отменять асинхронные операции, если эти элементы выходят из поля зрения или за пределы страниц, хранящихся в буфере. Достаточно сказать, что производительность элемента управления ListView была тщательно протестирована!

Также важно напомнить еще раз, что во всех этих цепочках мы используем then, потому что функция отрисовки в свойстве renderComplete возвращает обещание. Эти функции отрисовки никогда не будут концом цепочки, поэтому done никогда не будет использоваться.

Пакетная обработка эскизов

Последняя возможность оптимизации — это воистину Священный Грааль для элемента управления ListView. В функции batchRenderer мы находим следующую структуру для renderComplete (большая часть кода опущена):

 renderComplete: itemPromise.then(function (item) {
    // mutate the element to update only the title
    if (!label) { label = element.querySelector(".content"); }
    label.innerText = item.data.title;

    // use the item.ready promise to delay the more expensive work
    return item.ready;
    // use the ability to chain promises, to enable work to be cancelled
}).then(function (item) {
    // use the image loader to queue the loading of the image
    if (!img) { img = element.querySelector("img"); }
    return item.loadImage(item.data.thumbnail, img).then(function () {
        // once loaded check if the item is visible
        return item.isOnScreen();
    });
}).then(function (onscreen) {
    if (!onscreen) {
        // if the item is not visible, don't animate its opacity
        img.style.opacity = 1;
    } else {
        // if the item is visible, animate the opacity of the image
        WinJS.UI.Animation.fadeIn(img);
    }
})

Она практически совпадает со структурой для multistageRenderer, за исключением загадочного вызова функции thumbnailBatch между вызовом item.loadImage и проверкой свойства item.isOnScreen. Наличие thumbnailBatch в цепочке указывает на то, что возвращаемым значением должен быть обработчик выполнения, возвращающий еще одно обещание.

Запутались? Ничего страшного, мы во всем разберемся. Но сначала мы должны побольше узнать о том, чего пытаемся добиться.

Если бы в ListView был всего один элемент, различные способы оптимизации загрузки были бы незаметны. Но в ListView обычно содержится множество элементов, и функция отрисовки вызывается для каждого из них. В функции multistageRenderer из предыдущего раздела отрисовка каждого элемента инициирует асинхронную операцию item.loadImage для загрузки эскиза из произвольного URI. Каждая такая операция может занять определенное время. Поэтому для всего списка может одновременно выполняться много вызовов loadImage, причем отрисовка каждого элемента будет ожидать завершения обработки соответствующего эскиза. Пока все понятно.

Важная характеристика, которая совсем не видна в multistageRenderer, состоит в том, что компонент img для эскиза уже находится в DOM, а функция loadImage устанавливает атрибут src этого изображения после завершения загрузки. Это в свою очередь инициирует обновление модуля отрисовки после возврата из оставшейся цепочки обещаний, которая после этой точки выполняется синхронно.

Может случиться так, что множество эскизов вернутся в поток пользовательского интерфейса через короткое время. Это приведет к усложнению работы модуля отрисовки и низкой производительности визуализации. Чтобы избежать этого, компоненты img необходимо полностью создать до их помещения в DOM, а затем добавить в пакеты для обработки в одном цикле отрисовки.

В примере это реализуется с помощью обещаний — функции createBatch. createBatch вызывается один раз для всего приложения, а ее результат (другая функция) сохраняется в переменной thumbnailBatch:

 var thumbnailBatch;
thumbnailBatch = createBatch();

Вызов этой функции thumbnailBatch, как я ее буду теперь называть, снова вставляется в цепочку обещаний функции отрисовки. Цель этой вставки с учетом природы кода пакетной обработки (как мы скоро увидим) — сгруппировать набор загруженных компонентов img, освободив их для дальнейшей обработки через подходящие интервалы. Если посмотреть на цепочку обещаний в функции отрисовки, вызов thumbnailBatch() должен вернуть функцию обработчика выполнения, возвращающую обещание. Значением выполнения этого обещания (если посмотреть на следующее звено в цепочке) должен быть компонент img, который можно добавить в DOM. Добавляя изображения в DOM после пакетной обработки, мы обрабатываем эту группу в одном цикле отрисовки.

В этом состоит важное отличие batchRenderer от функции multistageRenderer из предыдущего раздела: в последней из этих двух функций компонент img уже существует в DOM и передается в loadImage как второй параметр. Поэтому когда loadImage устанавливает атрибут src изображения, инициируется обновление отрисовки. А в функции batchRenderer элемент img создается отдельно в loadImage (где атрибут src также установлен), но элемента img еще нет в DOM. Он будет добавлен в DOM только после выполнения thumbnailBatch, что делает его частью группы на этом этапе разметки.

Теперь рассмотрим, как действует пакетная обработка. Далее представлена полная функция createBatch:

 function createBatch(waitPeriod) {
    var batchTimeout = WinJS.Promise.as();
    var batchedItems = [];

    function completeBatch() {
        var callbacks = batchedItems;
        batchedItems = [];
        for (var i = 0; i < callbacks.length; i++) {
            callbacks[i]();
        }
    }

    return function () {
        batchTimeout.cancel();
        batchTimeout = WinJS.Promise.timeout(waitPeriod || 64).then(completeBatch);

        var delayedPromise = new WinJS.Promise(function (c) {
            batchedItems.push(c);
        });

        return function (v) {
            return delayedPromise.then(function () {
                return v;
            });
        };
    };
}

Опять функция createBatch вызывается всего один раз, а ее результат, функция thumbnailBatch, вызывается для каждого отрисованного элементав списке. Обработчик выполнения, формируемый thumbnailBatch, вызывается после завершения каждой операции loadImage.

Такой обработчик выполнения можно было бы легко вставить в функцию отрисовки, но мы хотим скоординировать операции для разных элементов, а не для каждого из них. Для этого в начале функции createBatch создаются и инициализируются две переменные: batchedTimeout, инициализируемая как пустое обещание, и batchedItems, инициализируемая как изначально пустой массив функций. createBatch также объявляет функцию completeBatch, которая просто очищает batchedItems, вызывая каждую функцию в массиве:

 function createBatch(waitPeriod) {
    var batchTimeout = WinJS.Promise.as();
    var batchedItems = [];

    function completeBatch() {
        var callbacks = batchedItems;
        batchedItems = [];
        for (var i = 0; i < callbacks.length; i++) {
            callbacks[i]();
        }
    }

    return function () {
        batchTimeout.cancel();
        batchTimeout = WinJS.Promise.timeout(waitPeriod || 64).then(completeBatch);

        var delayedPromise = new WinJS.Promise(function (c) {
            batchedItems.push(c);
        });

        return function (v) {
            return delayedPromise.then(function () {
                return v;
            });
        };
    };
}

Теперь посмотрим, что происходит в функции thumbnailBatch (возвращаемой createBatch), которая опять вызывается для каждого отображаемого элемента. Сначала мы отменяем любую существующую функцию batchedTimeout и сразу же повторно создаем ее:

 batchTimeout.cancel();
        batchTimeout = WinJS.Promise.timeout(waitPeriod || 64).then(completeBatch);

Во второй строке показан шаблон будущей доставки или выполнения, описанный в записи "Все об обещаниях" <TODO: link>: функция completeBatch вызывается после задержки в waitPeriod мс (значение по умолчанию — 64 мс). Это значит, что если thumbnailBatch опять вызывается в течение waitPeriod с момента предыдущего вызова, batchTimeout сбрасывается и устанавливается другое значение waitPeriod. Поскольку функция thumbnailBatch вызывается только после завершения вызова item.loadImage, можно сказать, что все операции loadImage, завершающиеся в течение waitPeriod предыдущего вызова, будут включены в один пакет. Если задержка превышает waitPeriod, выполняется обработка пакета (изображения добавляются в DOM) и начинается следующий пакет.

После этого thumbnailBatch создает новое обещание, которое просто записывает функцию диспетчера выполнения в массив batchedItems:

 var delayedPromise = new WinJS.Promise(function (c) {
         batchedItems.push(c);
     });

Помните, в записи "Все об обещания" <TODO: link> было сказано, что обещание — это просто конструкция кода. Это можно сказать и сейчас. Новое обещание не содержит асинхронных операций: мы просто добавляем функцию диспетчера выполнения, c, в batchedItems. Но, разумеется, мы ничего не делаем с диспетчером, пока batchedTimeout не будет выполнена асинхронно. Так что здесь есть асинхронная связь. Когда истекает период ожидания и мы очищаем пакет (в функции completeBatch), мы вызываем все указанные обработчики выполнения в методе delayedPromise.then.

Перейдем к последним строкам кода в createBatch — функции, возвращаемой thumbnailBatch. Эта функция — обработчик выполнения, вставляемый в цепочку обещаний функции отрисовки:

 return function (v) {
           return delayedPromise.then(function () {
               return v;
           });
       };

Давайте добавим этот фрагмент кода напрямую в цепочку обещаний, чтобы увидеть полученные связи:

 return item.loadImage(item.data.thumbnail);
          }).then(function (v) {
              return delayedPromise.then(function () {
                  return v;
              });
          ).then(function (newimg) {

Теперь мы видим, что аргумент v — это результат item.loadImage, т. е. компонент img, созданный для нас. Если бы мы не хотели выполнять пакетную обработку, мы могли бы просто добавить выражение return WinJS.Promise.as(v) , и вся цепочка все равно бы работала: аргумент v передавался бы асинхронно и отображался как newimg на следующем этапе.

Вместо этого мы возвращаем обещание из delayedPromise.then, которое не будет выполнено (с аргументом v), пока не выполнено обещание batchedTimeout. В это время (если между завершением операций loadImage есть задержка не меньше waitPeriod) данные компоненты img передаются на следующий этап цепочки, где они добавляются в DOM.

Вот и все!

Заключение

У пяти разных функций отрисовки, продемонстрированных в примере оптимизации производительности HTML ListView, есть одна общая черта: они показывают, как асинхронная связь между ListView и функцией отрисовки, выраженная обещаниями, делает функцию отрисовки невероятно гибкой с точки зрения способа и времени создания элементов в списке. При написании собственных приложений стратегия оптимизации ListView сильно зависит от размера источника данных, сложности самих элементов, объема получаемых асинхронно данных для них (например, при загрузке удаленных изображений). Очевидно, для достижения необходимой производительности функции отрисовки элементов должны быть максимально простыми. Но в любом случае теперь у вас есть все инструменты, необходимые для обеспечения оптимальной производительности ListView (и вашего приложения).

Крэйг Брокшмидт (Kraig Brockschmidt)

Руководитель программы, рабочая группа по экосистеме Windows

Автор книги Programming Windows 8 Apps in HTML, CSS, and JavaScript (Программирование приложений для Windows 8 на HTML, CSS и JavaScript)