Over at Keylime Toolbox, we have a feature that lets you test filter patterns against your query data. To make it “fast” we limit the data to the most recent day of data. But this can sometimes be 50,000 or more queries. So when rendering all those into a list (with some styling, of course), it would make the browser unresponsive for a time and sometimes crash it.
After hours of debugging and investigating options, I finally fixed this by limiting the number we render to start with and then adding “infinite scroll” to the lists to add more items as you scroll.
I looked around for existing solutions and found Marc Grabanski’s post from Sprint.ly about how they made their lists faster using on-the-fly rendering of items. I tried to do something like that but I couldn’t even get the basic lists to render. When I added a list with 50,000 empty, non-styled items to the page it killed the page. From my investigation with Chrome profiler this appears to be something in our webshims library that’s firing a resize event on every DOM change. You can imagine why the browser can’t keep up.
My solution was to only render one hundred items in each list to start. Then, when you scroll to the bottom of the list, it adds more items. It turned out to be somewhat straightforward, so I’ll share some example code.
We use an AJAX call to get the data and an event on return. The success event looks something like this. We load all the data into an instance variable. Then render the framework for holding the data (the match_results
template). Then we render the first 100 items the list. Finally I bind an event to the scroll for the lists.
updateResultsWith: (data)->; @matchData = data results = JST['match_results'] data: @matchData @$('#results').html(results) @renderMatches() @$('#results_lists ul').on('scroll', @onResultsListScroll.bind(@))
The renderMatches
method adds the next 100 items to the list. I’ll show that in a minute. The template is pretty basic and could be as simple as a ul
.
#results_lists %ul#matches
Now the fun comes in the scroll event. When the scroll hits the end of the page (using a hint from this SO post), we add more content to the list.
onResultsListScroll: (event) -> $target = $(event.target) if $target.scrollTop() + $target.innerHeight() >= event.target.scrollHeight - 20 @renderMatches()
Because we’re waiting until the end of the scroll, we don’t have to mess with _.debounce
or requestAnimationFrame
like Marc did in the post I mentioned on top.
The magic of all this comes together by appending the next 100 items to the list whenever we call renderMatches
. It turns out that’s actually pretty straightforward when I use JST to mix some coffescript into my templates.
renderMatches: -> count = @$('#matches').find('li').length results = JST['list_items'] data: @data, offset: count @$('#matches').append(results)
The list_items
template could be as simple as this:
- for query in @data[@offset..@offset + 100] %li= query.string
Some thoughts on this.
First, I realize that eventually, you could have 50,000 items in the list again and that would slow down the page and the rendering and maybe kill the page. But the user has to scroll through them which I don’t actually believe that anyone will do. In testing, I got up to 20,000 items on the page and didn’t crash the browser (although my laptop got pretty hot), so perhaps this batch-loading addresses the original problem.
Second, by only rendering the first 100 items, I realize that we’ve lots the ability to Ctrl-F
search for specific values. I don’t know if that’s a use-case so will talk to our customers about that and adapt if needed.