An approach to handling long lists in Vue.js (2.x) (hint: Vanilla JavaScript)

Vue.js is an excellent front-end web application framework and my framework of choice.

However, one area where it falls down is rendering long lists. Typically, I experience issues when a list has over 1,500 times.

My understanding is the problem revolves primarily around reactivity of properties and how that impacts rendering. I can’t remember the technical details but a Google/Bing search for “vue.js render long lists” will soon provide StackOverflow answers that explain it better than I can.

I also get the impression that long lists is not a common problem in most web applications. Unfortunately, my development for Lotus has me needing to render a few very long lists for back-end reporting and debugging. A typical timesheet (the core business of Lotus) can include over 2,000 (and sometimes up to 4,000) data records which need to be rendered in a single, continuous (un-paginated) scroll list for analysis.
And we now have a couple of internal reports that can include up to 10,000 items.

Ideally some of these reports will be eventually re-architected to include either pagination or better sub-filters. For now it’s a matter just making them available for viewing and download, so the long-list problem is one I need to contend with.

 

My Approach: Vanilla JavaScript

Earlier attempts to remove Vue.js reactivity via Object.freeze() (after following per blog and StackOverflow suggestions) didn’t seem to improve performance. I don’t know why, and I don’t have time to dig into it.

So I came up with a simpler solution that simply cut out Vue altogether: I used plain old vanilla JavaScript.

Now, an important factor to remember is: once I’ve rendered a list, I don’t need to update it again. So reactivity and and event binding are not a concern for me in these situations.
Well, that’s not quite true. In once instance I do need to update a CSS class based on a click event in a table row, but that too was handled easily with Vanilla JS (explained below).

Rendering is super simple.

1. Create a target element with an ID in your app/component, like this: <div id="vanillaJsRender"></div>

2. Load your data in your Vue app/component as normal, then create a Vue component method to call after the data is loaded, that iterates over the data, generates a string with your HTML to render, like the following simple example:

var html = "<ul>";
for (var i = 0; i < data.length; i++)
{
    html = html + "<li class='row-" + data[i].ID + "' onClick='onReportResultsRowSelectVanillaJs(" + data[i].ID+ ")'>" + data[i].MyValueToDisplay + "</li>";
}
html = html + "</ul>";

3. The insert the HTML into the target element like so: document.getElementById("vanillaJsRender").innerHTML = html;

It’s not pretty, but it’s simple and it works. And render is much, much, much faster than a Vue v-for loop.

Something I’ve noted with this approach is scrolling 10,000 records with of rendered HTML still causes some level of lag in the browser, though to be expected, and it’s manageable and not at bad as the Vue-based alternative.

 

Binding click events/updating CSS classes

Note the following class='row-" + data[i].ID + "' onClick='onReportResultsRowSelectVanillaJs(" + data[i].ID+ ")' added to the <li> in the sample above.

If I want to apply or remove a CSS class to a row in the HTML rendered outside of Vue, I just need a function like the following within the standard <script> section of our Vue app or component.

//-- Vanilla JS onClick handler
window.onReportResultsRowSelectVanillaJs = function(id)
{
    // Vanilla JavaScript to add/remove the `selected` class on the row
    var row = document.querySelector('.row-' + rowId);

    if (row.classList.contains("selected"))
    {
        row.classList.remove("selected");
    }
    else
    {
        row.classList.add("selected");
    }
}

Again, we’re using Vanilla JavaScript to manipulate the DOM and it works fine alongside Vue.

Note the window in window.onReportResultsRowSelectVanillaJs. We can’t simply define a function onReportResultsRowSelectVanillaJs() { ... } because the scope is not global. We need a globally scoped function (hence the attaching to window) for it to be accessible via onClick.

Just remember to name the function something that won’t cause potential naming conflicts in the global namespace.

I could have bound the function to the target element via a DOM addEventListener() method, but the way I did it was simple and it works perfectly well for my situation.

 

Another Approach: Server-Side Rendering

Before trying the above JavaScript approach I also tried generating the HTML a server-side.

To be clear, the app is a very clearly separated HTML front-end that is driven entirely by Vue.js, and backend that is pure API and ordinarily performs no HTML rendering (e.g. it’s not an MVC web application).

So in my server-side rendering approach, the API that is called would normally return a set of structure data but instead returns a string that is the HTML to be injected into the page. Again, the injection is performed via document.getElementById("vanillaJsRender").innerHTML = html.

Why take this approach? I come from an old-school web application background where all HTML used to be server side rendered, so this is always an option. And recently I’ve been revisiting blogs and talks by DHH (David Heinemeier Hansson) and has reminded that rendering on the server is often a fuck load faster than in the browser.

Also, why not?

 

Conclusion

There’s no profound conclusion. I simply took other experiences and thought a little ways out of the box (the Vue box) to solve a problem.

The solutions may not be elegant to some people but they’re simple, they work, and they solve the problem, which at the end of the day is what we’re all about as software developers.