Telerik blogs

The timeline has become a ubiquitous part of our user experience. It started with Facebook and Twitter, and now Pinterest and the Google+ interface have given us a new take on the same concept: display a list of items to the user. As they scroll down the page, add in the next batch of items. Sometimes those new items are added automatically (ala "endless scrolling"), and often there is a "push to load more" button, like in Instagram's new website.

Some of the Kendo UI widgets support this concept of adding new pages of data to the bottom. Specifically, the grid which has "virtual scrolling", and the mobile ListView which has a "press to load more" option.

The Kendo UI Web ListView, however, does not have this built into the widget. It's rather easy to add without having to write much code, but you can find yourself with perf problems if you aren't careful with how you implement it. Lets take a look at how you might think it should be done, vs how to do it and get the best possible performance.

Finished Product

When we are done, we will have a list of items that pulls from the Twitter Search API. There is a press to load more button at the bottom which will retrieve the next page of results, but will append them to the bottom of the listView.

Appending Items

When I first implemented this, I had one DataSource which read from the endpoint. Then I just used a simple jQuery get operation to retrieve the next set of items. When the request came back, I looped though and added each item to the DataSource. Adding the item to the DataSource would automatically update the UI. It was simple.

First Passs

<div data-role="listview" data-template="template" data-bind="items"></div>

<script type="text/x-kendo-tmpl">
  // template removed for brevity
</script>

(function($) {

  // set the page number
  var page = 1;
  var url = "http://sampledata.com?page=";

  var viewModel = kendo.observable({
    tweets: new kendo.data.DataSource({
      transport: {
        read: {
          url: function() {
            return url + page;
          }
        }
      }
    }),
    load: function() {
      // increment the page
      page = page + 1;

      // execute a simple get for the next page of data
      $.get("http://sampleData.com?page=" + page + ", function(data) {
        // get a reference to the datasource
        var ds = viewModel.get("tweets");

        // add each item to the datasource
        $.each(data, function() {
          ds.add(this);
        });
      });
    }

  }

}(jQuery));

However, I noticed a major problem as I went along. The first call for data was fast. The second was fine. By the time I appended the third page of data, I realized I officially had jank.

What Is Jank?

"Jank" is the word that we have come to use to describe poor performance in the browser. There was a session on "Jank Busting" at I/O 2013 that is free to watch. I would highly recommend taking the time to watch it so that you can get a basic understanding of how the browser works, what actually causes "jank", and what tools you have to get rid of it.

In the case of adding more items to the Listview, the page would literally being to completely hang while new items were being added. There are a few different reasons for this, but to fully understand how to make it fast, we need to understand why calling dataSource.add over and over again can lead to sluggish behaviour.

Paint The Fence



Whenever an item is added to the DataSource, it triggers the "Change" event, which subsequently causes Kendo UI to run the current item through the template, generate the HTML and append it to the DOM. The browser then paints the pixels on the screen required to display the HTML. It does this for each and every item. It's just that it happens so quickly that it looks like it's adding all the items at once. If you were to put a break point in the change event and step through each item, you could see each one individually drawn to the screen.

The bottom line is that painting is an expensive operation for the browser. Each time you ask the browser to paint something on the page, you are taxing it. This is especially true if your template is quite large. Instead of painting each individual item, it would be far more effective to generate all of the HTML for the items and then ask the browser to paint them all at one time. So how do we do that using the Kendo UI DataSource?

Let's Get Creative

The Kendo UI DataSource does not have a built-in method for adding a bulk amount of items. Like yourself, I run up against problems with Kendo UI that I don't immediately know the answer to. I usually head over to StackOverflow as it's really the best place to see if someone else has already tried and figured out how to do something. In this case, Kendo UI MVP Ona Bai had already figured it out.

$.merge

jQuery has a little known utility method called merge that will combine two arrays. It takes two arrays and combines the second with the contents of the first by appending them at the end.

In the case of appending new pages of items onto the ListView, the trick is to use two DataSources instead of one. The first DataSource merely reads the first page of data. The second one is responsible for each subsequent read and takes care of appending the results onto the first DataSource using the merge function. The magic is where the data method of the first DataSource is called and the modified collection is passed in. This causes the change event to fire, and thusly a single paint occurs.

Here is the complete code from the Twitter Search sample...

Appending Items With A Merge

(function($) {
  
  // high level parameters
  var searchURL = "http://demos.kendoui.com/service/Twitter/Search";
  var maxIdRegExp = /max_id=\d*/,
  sinceIdRegExp = /since_id=\d*/;

  // utility function to get max_id from query string
  function getQueryValue(queryString, regexp) {
      var value = regexp.exec(queryString);

      if (value) {
          value = value[0].split("=")[1];
      }

      return value;
  }
      
  // define a common transport
  var read = {
    url: "http://demos.kendoui.com/service/Twitter/Search",       
    // the remote service url - Twitter API v1.1
    contentType: "application/json; charset=utf-8",
    type: "GET",
    dataType: "jsonp"
  };

  // define a common schema
  var schema = {
    parse: function(response) {
      // get the max_id if it exists
      if (response.search_metadata) {
          max_id_str = getQueryValue(response.search_metadata.next_results, maxIdRegExp); //retrieve max_id for the endless scrolling
      }
      return response;
    },
    data: function(data) {
      return data.statuses || [];
    }
  };

  var viewModel = kendo.observable({
    tweets: new kendo.data.DataSource({
      pageSize: 5,
      serverPaging: true,
      transport: {
        read: read,
        parameterMap: function(options) {
          return {
            q: "html5",
            count: options.pageSize
          };
        }
      },
      schema: schema
    }),
    loadMore: function() {
      this.get("more").read();
    },
    more: new kendo.data.DataSource({
      transport: {
        read: read,
        parameterMap: function() {
          
          var parameters = {
            q: "#html5",
            count: viewModel.get("tweets").pageSize(),
            max_id: max_id_str
          };
  
          return parameters;
        }
      },
      change: function(e) {

        // when the data is returned, get a reference
        // to the first datasource and it's items
        var tweets = viewModel.get("tweets");
        var existingItems = tweets.data();
        // newItems is a reference to the items that were just returned
        var newItems = e.items;

        // call the jQuery merge method to combine
        // the two arrays into one
        $.merge(existingItems, newItems);

        // call the data method on the first datasource
        // which triggers the UI to update
        tweets.data(existingItems);
      },
      schema: schema
    })
  });

  var max_id_str;

  kendo.bind(document.body, viewModel);
  
}(jQuery));

Keeping Your Eye On Performance

It's important to at least be cognizant of when your application begins to janky, and then to figure out why it's happening. Kendo UI does a really good job of taking care of most of this for you, but custom solutions always require being aware of how the browser works, and what you can do to vastly improve the responsiveness of your app.


Burke Holland is the Director of Developer Relations at Telerik
About the Author

Burke Holland

Burke Holland is a web developer living in Nashville, TN and was the Director of Developer Relations at Progress. He enjoys working with and meeting developers who are building mobile apps with jQuery / HTML5 and loves to hack on social API's. Burke worked for Progress as a Developer Advocate focusing on Kendo UI.

Comments

Comments are disabled in preview mode.