April 12, 2013
by Matt Sacks

Our Infinite <table>

our-infinite-table

In the Simple web app, we display a customer’s transactions in a long infinitely scrolling list. Though there are many different implementations of infinite lists on the web, we still ended up rolling our own unique solution. Before I get into the technical details of what makes Simple’s activity list different, I want to explain why we couldn’t use a normal <table> element.

The Issue

A few months ago, we noticed that the user interface was dramatically slower for customers with a large number of transactions. Any customer with 300 or more transactions would notice a pause between clicking a transaction and seeing it become selected (an average of ~225ms). Yet when only a few transactions were displayed (for example, when searching “last 7 days”), it would only take a fraction of the time. Clearly, the number of transactions in the list affected performance.

Our activity list is structured within a <table>, with each transaction being its own <tr>. Inside each of those transactions are about 15 elements, so for 300 transactions you’re looking at 4,500 DOM elements. Any modification to the <table> (such as adding a class, removing an element, etc) would take a noticeable amount of computation time because the browser has to recompute the style of the entire table. Also, this would only increase with the number transactions a customer has. It was clear that dramatically reducing the number of transactions rendered was the key to making interaction with the activity list faster.

Trials and Errors

Our first attempt was the one most commonly found in infinite list implementations: using position: absolute; to position the table rows according to their index. Since the height of each of cell in our activity list is static, it was easy to get a prototype working.

But, we use a <table> element for our activity list and not a <ul> (as other infinite scroll implementations do) so that we can use both fixed and variable widths in our table cells since the width of the amount column varies. When using absolute positioning, those rows are no longer relative to one-another which results in misalignment.

Alignment of non-fixed width cells when the table rows are absolutely positioned

Alignment of non-fixed width cells when the table rows are absolutely positioned.

However, rendering and removing individual cells seemed to be the way to go since it would reduce the number of elements in the <table>. While looking around for existing models of this concept, we found iOS to be an inspiration for how it handles scrolling lists with adding and removing elements.

The controller responsible for lists in iOS is called UITableViewController, yet when searching the web for UITableViewController in JavaScript you won’t find many results. A reason for this could be because window.onscroll doesn’t fire for every pixel scrolled.

The approach for adding and removing individual table cells is based on the change in scrolled pixels between callbacks to window.onscroll. This starts to fail when the page is scrolled quickly because the computation required to render individual cells exceeds scroll speed.

In the loop, you have to calculate where you are in the page and how many cells to add and remove based on the difference in scroll amount from the last time the event was called. This is a calculation takes over 150ms to render which becomes very noticeable while scrolling the page, even with throttling.

Our Solution

So we stepped back from the technicalities and realized that we only need to render as many transaction cells as there are on the screen. Everything off-screen is unnecessary and just makes the web app slower.

We render as many transactions as can be displayed on the screen and nothing else. When the customer scrolls, the list is re-rendered with any transactions that would be indexed at the scroll level in the page, which is simple because each transaction cell is a static height. Rendering a <table> on scroll can be expensive so we throttle how often we refresh it.

Here’s how it works: the viewport is the on-screen space that the activity list takes up in our app. Above and below the viewport are a few additional transactions hidden behind the scenes so that when scrolling quickly, the list doesn’t show a blank area at the top and bottom right away. Finally, there are bumper elements on the top and bottom with calculated heights to make the <table> appear as tall as it needs to for rendering all transactions.

When scrolling down the page, we increase the height of the top bumper and decrease on the bottom one, vice versa for scrolling up. These elements allow the customer to scroll the list and gives us an accurate window.scrollY coordinate rather than using JavaScript to fake scrolling by positioning the top style. Here’s a mockup of what the implementation looks like:

Mockup of the implimentation of the table solution

The Result

This implementation has had great results: when we manipulate our <table>, previously slow computations now take 1/10th the time as the unmodified element.

Screenshot of the before and after view of the timeline in Chrome's dev tools

Because the transaction list is so central to the Simple experience, this improved speed in many areas, such as initial page load and search.

There are some pitfalls to replacing our activity list on scroll. One is that any hover styles currently being applied will blink due to the element being re-rendered. Another is that the more pixels the list spans (larger monitors and retina displays), the more time it takes to refresh the table.

But what does the actual performance look like? In Chrome v26, there’s a slight noticeable pause between the customer ending his or her scroll and the transaction list getting updated. Yet this seems to be nonexistent in Canary, so this should improve as Chrome continues to be updated. Safari and Opera both seem to flicker heavily when rendering on scroll. Firefox, on the other hand, seems to handle this operation splendidly and scrolls very smoothly.

Besides the technical benefits this change has brought, our customers have noticed improvements in speed. While speed is usually a good thing, providing our customers with a noticeably better experience is always a good thing.