UITableView in JavaScript, list view with re-usable cells using flexbox

If you’re familiar with iOS development you will know that a UITableView is very efficient when displaying a list of data. A simplification of what it does is display enough cells to fill the viewport plus a few more either side. As you scroll it re-uses cells that are now out of the viewport so a list with thousands of items will only ever use a fixed amount of cells. Highly recommend reading The fine art of UITableViews. Now this has certainly been done before in JavaScript, the best known project being infinity.js, but my approach takes an interesting turn, I avoid heavy DOM operations by using flexbox.

How does flexbox help?

If you’re familiar with flexbox you may have come across the order property which allows you to reorder flex items in a flexbox container and this is the magic that allows us to create reusable list items without having to actually rip the element out of the DOM and then re-inject it in the right place as a user scrolls.

Check out the project on github and have a look at the live demo

How does it work

The whole technique boils down to the flexbox order property which allows reordering of a flex item within a flexbox container, essentially I can say to the browser repaint the DOM node here instead of where it is in the actual DOM structure.

flexbox order property example in Chrome Dev Tools

In the above image the top four cells are actually rendered beneath the last four as we’ve informed the browser to repaint them below using the order property.

On scroll I check the scroll position. When the user has scrolled more than the threshold, (3 cells are out of the viewport) I then set the order property on the element that can now be painted at the bottom of the list. To then render the correct data the order number and the array index is passed off to renderCellFn().

First we check the user is actually scrolling and in which direction.

onScroll: function onScroll() {
    scrollTop = body.scrollTop;
    this.direction = scrollTop - lastScrollTop;
    this.checkCells();
}

The scrolling direction is determined by comparing the last scroll top position to the current. The onScroll method is what the scroll event calls directly.

getCurrentCell: function getCurrentCell(count) {
    return this.cells[count % this.cells.length];
}

In checkCells() the getCurrentCell() method is called to determine the current cell by using the modulus (%) operator. Previously I had some over complicated maths to determine which of the re-useable cells I could use but all I really needed was modulus. What it does is divide the current count of “cells” that are out of view by the number of re-useable cells and returns the remainder which is the cell I can then reorder and render the data in.

Once we have the correct cell to use we then determine if it’s far enough out of the viewport to be able to use by calling isTopElementOutOfViewport().

isTopElementOutOfViewport: function(el) {
  var elemPostion = el.getBoundingClientRect(),
      bottomOffset = elemPostion.bottom - (this.container.offsetTop - body.scrollTop);

  return !!elemPostion && bottomOffset <= -(this.cellReorderThreshold());
}

To determine if it’s out of viewport we use the getBoundingClientRect() which will return a bunch of metrics about the cell we’re looking to use. This sort of operation is normally expensive and will cause a layout to trigger but since our actual DOM is so small it’s insignificant for the browser to do.

this.cellsOutOfViewportCount++;
this.cellIndex = this.cellsOutOfViewportCount;
this.elementStyle.paddingTop = parseInt(this.elementStyle.paddingTop || 0, 10) + this.CELLHEIGHT + 'px';
this.currentCell.style[orderProp] = this.cellIndex;
this.renderCell(this.currentCell, this.cellIndex-1);

If the top cell is far enough out of view then we can trigger the reorder, render the new data and increase the lists top padding to maintain the correct scroll position.

Why not use requestAnimationFrame?

You may be asking why I don’t use requestAnimationFrame when calling checkCells(). In Firefox the performance degraded massively and I’m not entirely sure why. So I’ve avoided it for the time being.

Performance and visualising the technique

In the screencast I show performance benefits of this technique when working with asynchronously loading data and comparing it with a regular technique for loading async data. I also cover and visualise how the cells are being reordered using the awesome chrome://tracing tool.

TL;DR Gif

Showing the cell re-ordering in chrome://tracing

Needs battle testing

The code base is fairly beta and quite usable but there is still some work to do, as I’ll list below:

Some of the issue and limitations are:

  • It only works with fixed height cells.
  • It has to be a flexbox supporting browser which is pretty good but no <IE10
  • iOS won’t trigger scroll events until after the page has stopped scrolling, this will require the use of a scrolling library.
  • Accessibility?
  • Only reacts to scroll events, keyboard users won’t see list updated.
  • Doesn’t render the cells if you refresh and the browser re-instates the scroll position.

We need this built into the Web Platform

If we want web apps to be performant then this elaborate hack should be built in to the Web Platform allowing the browser to handle the logic behind re-using cells without killing performance. We could introduce a property on ol/ul or even have a new element that treats any children as a re-usable cell.

There are some early editor drafts of specs that may help with this, the two I know of are css-containment and the will-change property.

Some of the MV* frameworks also have components for working with large data sets but most have the restriction of fixed height cells due to heavy recalculation issues when using variable height cells. Ember has list-view, Angular has ng-scroller. Both of these libraries remove and re-inject DOM nodes.

Paul Irish has compiled a good list of libraries and list of Do’s and Dont’s to reduce jank when scrolling