Using ListView (HTML)
The ListView control can present items in a vertical or horizontal list and lets users select and manipulate individual items. The control works with a data source and a template that you define to convert the data items into their visual representation. This topic describes some aspects of the ListView control tthat you can tweak to improve performance.
List performance
When working with a ListView control, there are a few places you can tweak to improve performance, including:
- Initialization – the time from when the control is created to items shown on screen
- Touch panning – the ability to pan the control by touch and having the UI keep up with the touch gestures
- Scrolling – the ability to use the mouse or keyboard to scroll the control and update the view
- Interaction for selecting, adding & deleting items
The ListView control depends on the app to supply the data sources and templating functionality to customize the control for that app. How those work with the ListView implementation has a large impact on the overall performance.
Optimize your data source
You use an IListDataSource to provide the ListView with data. The IListDataSource can work with a range of sources of data, and can support virtualization, editing, and change notifications. The IListDataSource can obtain data from asynchronous and synchronous data sources.
Synchronous data sources (Binding.List)
A synchronous data source is one that already has all the data available, ready to be returned or is interfacing with a near-instant source of data, such as a native component exposed with a Windows Runtime API. The Windows Library for JavaScript includes a built-in data source, WinJS.Binding.List, which accesses an in-memory array of data. Provided that the data set isn't too large, WinJS.Binding.List is the easiest way to supply data to the ListView control. Because the data for WinJS.Binding.List is stored in a JavaScript array, it does not scale well to very large data sets, and you should consider a custom datasource if you have more than a few thousand items. A custom data source can be synchronous and return its results immediately, but this works well only when the results are truly immediately available. The ListView and data source code are mostly executed on the main UI thread, and synchronous data source operations block the UI thread, preventing the application from responding to the users and appearing to be hung. Therefore, if there is any chance that the data is not immediately available, access the data asynchronously.
Working with asynchronous data sources
To use the ListView control to display a custom source of data, you can create an object, known as a custom data source, that provides the logic to provide the data to the ListView control. The ListView uses two interfaces to obtain data: IListDataAdapter and IListDataSource. To create a custom data source, you :
- Create an object that implements the IListDataAdapter interface.
- Create an object that inherits from VirtualizedDataSource and pass it your IListDataAdapter object. VirtualizedDataSource implements the IListDataSource interface for you.
(For complete instructions, see How to create a custom data source.)
This is the easiest way to implement a custom data source for use with ListView. The next sections describe how to improve performance when developing the data source.
Optimize batches
The main way for the IListDataSource to retrieve items from the IListDataAdapter is to use these functions:
- PromiseitemsFromIndex(index, countBefore, countAfter)
- PromiseitemsFromKey(key, countBefore, countAfter)
These methods retrieve a set of items. They request a specific item by index or key, and ask for a batch of records to either side of the requested item. Treat countBefore and countAfter as estimates. The IListDataAdapter can supply more or fewer records than requested, if that's the more efficient way for it to fetch data. The methods return a IListDataAdapter for an IFetchResult object that contains an array of records and additional metadata. If additional data is supplied, then it is usually cached by the IListDataSource for later use. If less data is supplied, then it passes the data it obtained back to the control (typically to render) and then makes subsequent requests for the data that it still needs.
Here are some examples of how an IListDataSource might call itemsFromIndex:
itemsFromIndex(0,0,1) – minimum batch size
This is a request for 2 items starting at index 0. If the IListDataAdapter is interfacing with a web service, then it may not be efficient to fetch only two records. So the data adapter may fetch records 0 to 9 from the web service, and hand them back. The IListDataAdapter will cache the additional 8 records in case they are requested later.
itemsFromIndex(40,1,200) – maximum batch size
This is a request for items 39 to 240. Fetching 201 items may cause a long delay while the server (or other source) calculates the data for the request. In this case, the IListDataAdapter might limit the results to batches of no more than 50 records at a time.
itemsFromIndex(40,1,200) – paged-based sources
We still reques items 39 to 240. But imagine that the data source is designed to return items based on a page-based model. Assuming the pages are 50 records each. Then the request t doesn't cleanly match the page boundaries. The IListDataAdapter must return the record at the index (in this case 40) so it must access the first page (0 – 49). It can return the records from just that page, or it can include results from multiple pages.
As can you can see from these requests, it's common to ask the IListDataAdapter for a small overlap of records compared to previous requests. The reason for the overlap is to help detect cases where items are inserted or deleted from the set of data between requests. It is better to supply the data, but if it's not supplied the overlap should not be automatically re-requested.
Incremental count
ListView requires a count of the records in the data set so that it knows where to set the bounds for scrolling. It may not always be possible for the data source to have an accurate count of items, particularly if the count takes time to calculate. The IListDataAdapter can estimate and return an inaccurate count to the IListDataSource, and can then update that count over time by using one of these techniques:
- Specifying a new count as part of the response to requests for items
- Sending insert or delete notifications to signal that additional items have been discovered, or that the count is less than previously supplied.
It's better to increment the count rather than to reduce it over time. If the a request is made for records and the IListDataAdapter returns fewer records, with a lower count, and has not sent delete notifications, then the IListDataSource will question where the records went, and will confirm the cached items are still valid by re-requesting them.
On-demand fields for records
Depending on your app, the records for items may have very large fields that can be supplied on demand. For example:
- An app might have thumbnail images for items stored as a binary Blob.
- A blogging app may have separate fields where one represents the summary for an entry and the other, the complete text
You might not want to include all the fields in the initial record fetches. Instead, you might use a separate call to supply this data as its needed. To get the best performance when updating your data, have the IListDataAdapter use events or Promise objects to supply that data rather than raising a changed notification for the record.
A changed notification causes the item to be re-rendered and performs calculations to see if the location should be updated for grouped views.
Updating/changing records
If the app needs to change records, it can make the changes inline or by replacing the entire record.
- If the IListDataAdapter calls IListDataNotificationHandler.changed to indicate that a record has changed, then it will force the entire record be re-rendered, and ListView will show a changed animation.
- Alternatively if the IListDataAdapter maintains object identity, it can update the field in the data directly, but will need to inform the UI tier to update the visuals for the item. If using data binding, then making a change using WinJS.Binding.as will cause the UI to update automatically.
This is a good technique to use for progressively updating the fields for an item. For example if your items represent images on the file system, and you wish to show a thumbnail preview of the image. Fetching the thumbnail may take time, so you may want to return the basic file data first, and then update the record with the thumbnail later.
Asynchronous fetch patterns
The ListView infrastructure was designed to minimize its use of the UI thread, especially while waiting for data. To provide a responsive user experience, use asynchronous methods whenever possible so that the IListDataAdapter doesn't block the UI thread. Many data-retrieval APIs, such as XMLHttpRequest (and the corresponding WinJS.xhr wrapper function), file system APis, and Windows Runtime APIs are mostly asynchronous.
If the data you need is supplied by a blocking synchronous API, give it an asynchronous wrapper. For example, call it from a Web Worker.
Working with grouping
The ListView can present items in groups. To enable grouping, ListView needs a separate data source that contains info about the groups and their sizes. (For more info, see How to group items in a ListView.) The ListView use the groupKey property of items to determine which group an item belongs to. It expects the data to be pre-sorted so that the items are presented contiguously for each group, and in group order.
Grouping can present performance challenges when:
Calculating the group info requires iterating over the items to retrieve their group data. ListView needs the info for any items on screen or in the off-screen buffer area. Part of the group information is the index of the first item in the group. If calculating the item offset requires iterating over the groups items, then that will cause a lot of data requests for items that are not technically required for the view. If it's using an asynchronous data source, and is in the middle of a large group, it might have to fetch a large number of records that are off-screen just to determine the group boundary.
When you use the SemanticZoom control.
It's common to use the SemanticZoom with a grouped collection to enable users to quickly navigate the list. This often means using the list of groups as the zoomed-out view. If the app needs to iterate over all the items to form the zoomed-out view, then it will likely have performance challenges.
The data being retrieved from the back-end data source is not sorted and accessed in the same order as required for the grouped view.
If the client app is required to re-sort the data, then it will not be able to perform effective data virtualization as it cannot depend on the order of the items being correct.
Items are inserted or updated.
Item insertions and edits are more time consuming for a grouped list than for a non-grouped lists because the item data must be verified to determine if the changes also modify the group membership.
Grouping works best when the data can be retrieved pre-grouped, and when the data about the groups can be retrieved quickly and independently of the data items.
Displaying items with a Template or render function
The ListView relies on a renderer that you supply to convert the data records into HTML for output. The way that ListView, the data source, and the renderer work together is critical to the performance experience of startup and scrolling, and to a large extent governs the interactive feeling of the application. The time taken to display a page of data will depend on the complexity of the item rendering, the number of items being displayed (a product of the screen size and item size), the performance characteristics of the data source, and the target hardware.
The way the app renders items can be optimized to improve the performance, or perception of performance of the app, by using techniques such as placeholders, element recycling, and progressive rendering.
There are two ways of create a renderer: declare a WinJS.Binding.Template, or create a custom render function.
WinJS.Binding.Template
To create a WinJS.Binding.Template, you use HTML markup to define a template for how an item is rendered. Within the template, you use data binding to associate properties of the HTML elements with fields in the data record.
The template is simple to use, but doesn't provide much control over how items are rendered. The template has limited support for handling placeholders. Data binding is synchronous when the data is supplied. Using the WinJS.Binding.Template with a synchronous data source results in completely synchronous rendering.
The template uses simple logic to detect whether items can be recycled, but it errs on the side of creating new items if there is a chance that recycling an item will present the wrong UI. For apps that contain complex items, data is supplied asynchronously, or if performance is a real concern, we recommend using a custom render function for the template instead.
Starting with Windows 8.1, the Windows Library for JavaScript 2.0 uses a more efficient system for processing WinJS.Binding.Template objects, and it significantly improves performance. With this system, data binding and control instantiation happens in a more fluid, parallel process rather than in series, as they did in Windows Library for JavaScript 1.0. If your code depends on the previous serial processing behavior, we recommend changing your code to take advantage of faster template processing. However, you can use the disableOptimizedProcessing property to restore the previous behavior if you can't change your code. For more info, see the HTML ListView item templates sample and the HTML ListView optimizing performance sample.
Custom render function
You can define custom render function for converting a data record into its HTML representation. The advantages of implementing a custom renderer are:
- It can perform customized element recycling.
- It can supply placeholder elements.
- It can render items progressively.
- It can make incremental requests for data, if needed.
The render function takes these parameters:
object renderItem(itemPromise, recycledElement)
- itemPromise: a IItemPromise for the data for the item to render. With a synchronous datasource, the IItemPromise is usually complete, but with an async datasource, it will complete at some time in the future.
- recycledElement : the DOM from a previous item that can be reused to display new content.
The render function must return either:
The root element of a DOM tree for the item.
An object that contains these properties:
element
: the root element of a DOM tree for the item, or a promise that when completed will return the root element for the item.renderComplete
: a Promise that completes when the item is fully rendered.
The render function executes once for each item. Here's an example of a custom render function:
function renderItem(itemPromise, recycledElement) {
// bare minimal placeholder work - create element with class to give it a color and size.
// and placeholder * { display: none; } or innerHTML = '' or removeChild() to hide the previous ui.
if (!recycledElement) {
recycledElement = document.createElement('div');
}
recycledElement.className = 'placeHolder';
var renderComplete = itemPromise.then(function (item) {
// Really quick paint of data.
// If your placeholder logic above didn't clear innerHTML you do it here.
// It would be good if it just did like 2 dom operations.
Utilities.removeClassName(recycledElement, 'placeHolder');
recycledElement.innerHTML = '<div class="title">' + item.data.title + '<div>';
return item.ready;
}).then(function (item) {
// This is where you do the more intense stuff
var imgEl = new Image();
// in css start img as opacity: 0;
recycledElement.appendChild(imgEl);
imgEl.addEventListener('load', function () { WinJS.UI.Animations.fadeIn(imgEl); }, false);
item.loadImage(item.data.someUrl, imgEl);
});
return { element: recycledElement, renderComplete: renderComplete };
}
Using a custom render function instead of a WinJS.Binding.Template provides several advantages:
You can use placeholders when data is not available
The purpose of the render function is to create the HTML for an item, but if the data isn't available, then we don't want to block user interaction while we obtain it. For example, the data might not be available if the app is still loading or if the user is scrolling quickly. When the data isn't available, the render function can provide a placeholder DOM structure while it waits for its data. The HTML provided by the custom render function doesn't need to be static for the lifetime of the item, so the render function sets up callback functions that can update the HTML as data becomes available.
The placeholder must be the same size as the final content and it must share the same root node. When populating the placeholder content, make as few changes to the DOM structure as possible to reduce the time it takes to lay out the item.
You can decide how to process the IItemPromise
Because the data might not be synchronously available, the render function is given an promise (in the form of an IItemPromise) for the data. When the promise completes, the data is passed to the IItemPromise object's then handler. This gives you the ability to decide how the render function returns its results:
If the render function uses a placeholder, it can register a handler for the IItemPromise that executes when the data is available. The renderer returns a placeholder immediately, and then uses the handler to update the HTML based on the data. For example, when enumerating the a list of files, it can show a placeholder for each file, and then update the placeholder with the filename as it becomes available.
If you want the render function to delay sending a result, it can return a Promise for the HTML element and complete that promise once the IItemPromise is complete. Here's an example:
function renderItem(itemPromise, recycledElement) { // ignore the recycledElement to make the example simpler var element = document.createElement('div'); var complete = itemPromise.then(function (item) { // wait for the data and form the structure for the item element.innerHTML = '<div class="title">' + item.data.title + '<div>'; return element; return { element: complete, renderComplete: complete }; }
You can decide how to recycle elements.
It's generally faster to update the properties of an existing DOM tree rather than creating a new DOM tree from scratch. For many items, only a small portion of the markup that renders the data is specific to a particular item. You can take advantage of this by making the function recycle these DOM elements. For example, suppose the ListView is scrolling through a set of items. As it scrolls away from items, it removes them from the tree, and as it scrolls toward items, it creates new markup to represent them. Rather than destroying the DOM tree for the old items and creating new items from scratch, the renderer can recycle an old item's DOM tree and update its content to display the new item's data.
Your render function opts into this capability by using the element supplied by the recycledElement parameter as the basis for the new item.
If you use element recycling:
- Clear out the old item's info before using the recycled element as a placeholder.
- If the HTML for an item has conditional state based on the item data, be sure to reset it when it's recycled.
- When recycling elements, minimize structural changes to the DOM. If the recycledElement is not appropriate for re-use, then ignore it and create a new element from scratch, and the recycledElement will be garbage collected.
You can delay expensive work.
Some items cost more to display. For example, if an app enumerates a set of pictures, then retrieving the name of an image is faster than fetching the image thumbnail. When scrolling quickly, prioritize fetching the basic data for each item over fetching the image data. If the item is scrolled off screen before the image can be fetched, you can cancel the retrieval.
Similarly, if the items contain complex structures, such as embedded controls, you might want to delay that work until all the basic state has been handled. For these cases, the IItemPromise provides a nested Promise called ready that doesn't completed until the basic rendering of other items is finished. This Promise can be chained and used to a signal that it's time to perform the more time-consuming work. The ready Promise is usually completed after all the items in the view have had their item promises complete.
You can cancel work if its no longer required.
If the renderer returns an object as it’s result, then one of the fields of the object is the
renderComplete
Promise. This Promise tells the ListView when it has completely finished rendering the item. The render function is responsible for completing the Promise after the render function completes all its work. If you use the ready Promise, the render function completes after the work performed by the ready Promise completes. Typically this Promise is formed by chaining together a set of Promise objects that will do the work in the renderer. The advantage of using a chain of Promise objects is that therenderComplete
Promise will be cancelled if the rendering of the item is no longer required. When a chain of Promise objects is cancelled, any functions in the chain that have not been executed will not run.
Cell-spanning view
When you set the ListView control's layout property to use a GridLayout, it can use the cell-spanning feature to display items of different sizes in the same list. Cell-spanned items must be integer multiples of the base size. For example, if the base size is 100 by 100 pixels, larger items might be 200 by 200 pixels, but not 150 by 200 pixels.
The ListView determines the size of an item using:
- The itemInfo function, if it was supplied.
- Otherwise, the size of the element returned by the rendering function.
When requesting data, the ListView uses the size of the common item to calculate the potential number of items that could be displayed; it uses this info to determine the number of items to request from the data source. Therefore, if the base size is set too small, the ListView will request more items than it actually needs. The ListView also uses a packing algorithm when displaying items that span cells. If the items have a range of sizes, it can take more time to compute item positioning, which makes layout passes take longer and degrades scrolling performance.