This article reviews different approaches to rendering long lists in front end web applications, and how such scenarios can be optimized. This article is an indirect follow-up to our earlier blog post on the topic of performance. In this blog post, we'll cover several approaches and discuss how they perform in real life scenarios.
When building complicated web applications, you might be faced with a task of rendering thousands of components to the same view. In Deveo - a code management platform for various version control systems - this is a requirement when rendering code changesets (aka diffs). In our case, this means a table containing anything from a single changed line of code (represented as a table row) to several thousand lines, while supporting dynamic functionalities like the ability to comment on each individual line. Our task was to identify the bottlenecks of the existing implementation and refactor it to be more performant.
For the sake of this blog post, we've created a simple Ember.js demo app to better illustrate the differences of each approach discussed below.
It's important to note that the benchmark results listed in this article might differ substantially between different setups due to machine specs, the OS and browser being used, and current system load. Also, the tools used to measure performance will add their own overheads. Therefore it makes sense to compare only the relative results if you're interested in doing testing on your own.
In the following sections, we'll go through the different approaches we considered when refactoring code changesets rendering in Deveo. More importantly, we'll use the demo app to demonstrate the pitfalls of "synthetic" benchmarks and how they differ from real-life applications.
First, let's take a look at the different approaches.
Approach #1: Incremental Rendering
The most efficient way to render scrollable, complex views is incremental rendering. In this approach, only the visible part of the page is rendered at a time, while maintaining scrollbar size and position in such a way that it imitates the whole page. As the user scrolls the page, more components are rendered into the viewport, while components now outside of viewport are removed from the DOM. This approach is often utilized in activity feeds and is usually the most performant of all options.
In the Ember world, this can be automated with ember-collection, virtual-each and smoke-and-mirrors. We've used ember-collection in our demo app.
In the React world, there are react-infinite and react-virtualized among many others.
Unfortunately, incremental rendering has several compromises. You'll have to consider the following:
In most implementations, all components have to be of either equal height or fixed height that must be known beforehand. Some implementations lift this requirement by guessing scroll size/position heuristically, making it inaccurate.
In-page search via browser's Command+F/Ctrl+F becomes impossible. Implementing custom search is a task that is hard to do right, both from the UI/UX and development perspectives.
Rendered components lose their internal state when removed from the DOM. In order to maintain the state after re-rendering, you'll have to store the component's state in the model layer, which increases the overhead.
During rapid scrolling, empty space becomes visible before it's filled with freshly rendered components. Although this visual artifact beats overall laggy scrolling of large prerendered lists, medium- and small-sized lists look better when prerendered.
For all implementations known to us, incrementally rendered lists must be homogeneous. You can't group items and give each group a heading.
In most cases, an incrementally rendered list requires its own scrollbar which may hurt the usability of your app: the user may scroll the container when they want to scroll the page and vice versa.
In our case, incremental rendering was not a viable option as we wanted to keep the browser's built-in search feature. We also bumped into some nasty rendering glitches. Hence we went looking for other approaches.
Approach #2: Rendering Multiple Components
This approach is the most straightforward practice and matched our existing implementation. In our case, this meant that every line in the changeset was an instance of the same component. Regardless of the framework/view layer you're using, component initialization bears a certain overhead. In Ember, the overhead is particularly large.
In our case, the implementation did not scale to thousands of components being rendered at once - the initial rendering simply became too slow.
To demonstrate this approach in practice, we can use the aforementioned demo app. We can benchmark the rendering performance by using Ember Inspector (a browser extension for debugging Ember applications) and its Render Performance tab. For example, on our test setup rendering 10 000 small, simple components takes 1 017 ms on an average of 5 runs. We will compare this result relatively to the other approaches in the next sections.
Approach #3: Rendering a Single Template With a Loop
Based on the internal benchmarks we did in our application, we concluded initialization of components being a bottleneck. In other words, most of the time was spent on scripting rather than rendering the components. Therefore we decided to avoid this problem by removing the components altogether.
We converted the component layouts into an array of objects, where each object contained pre-generated HTML for the specific diff line. This array was then looped over in the template, simply rendering HTML only.
In our benchmarks, this approach resulted in a much better performance than the previous solution, but it was still not quite what we were after.
In the demo app, this approach is roughly 3x faster than the previous approach with an average rendering time of 327 ms, due to lack of component initializations.
Approach #4: Inserting a String of HTML
As rendering an array of objects in the template using an
each loop still seemed to slow things down, we came up with the idea of generating a single concatenated string of HTML beforehand and inserting it directly into the DOM.
Benchmarking this approach in the demo app, the rendering time improves to a whopping 19 ms! Compared to the initial result of 1017 ms, this approach indicates a 53x performance boost.
These numbers are reported by Ember Inspector and seem to be equivalent to the Scripting portion of Chrome's Performance report.
Given the promising results of the prototype we made at that time, we concluded string generation being extremely fast when compared to the other solutions and decided to move forward with the approach. Furthermore, we knew the difference would become more substantial the more data (in our case, diff lines, or table rows) there'd be.
We were struck with joy until we found out that the benchmarks we conducted using the prototype we had did not translate to the actual performance of our application at all.
Practical Results: Where Did Our Performance Go?
When we implemented HTML string insertion in the actual application and measured the difference, the improvement turned out to be only 2-4x over the existing solution. This was drastically different compared to, for example, the 53x improvement mentioned earlier.
All the synthetic benchmarks indicated great results, so what happened?
We started digging further and profiled each approach using Chrome's Performance tool. The following is what we found, also demonstrated in the demo app as the styled counterparts.
In the demo app, we've used fragments of the TodoMVC app's HTML and CSS to better imitate DOM size of real-life applications. After all, actual applications tend to have more complex layouts with CSS styles, and not just simple HTML.
Multiple Styled Components
According to Ember Inspector, rendering multiple styled components with more complex layouts is roughly 12x slower when compared to simple ("synthetic") components. This is a huge difference!
The above is a screenshot of the performance profiling done in Chrome. When comparing the above profile to a profile made from multiple unstyled components (the first benchmark):
It's no surprise that more time is now being spent in the Rendering routine rather than in Scripting when layout and styles get more complicated.
Single Template With Styles
Benchmarking this approach in the demo app reveals that it is on average only 1.16x faster than the multiple styled components one. This is drastically different compared to the unstyled benchmark where the gained improvement was over 3x.
Furthermore, based on this we can conclude the following:
The more complex the layout and CSS styling in each component, the less impact component initialization has on the total rendering time.
This is more prominent in components that contain very little logic, but complicated layout and styles.
Injecting a String of Styled HTML
The results here are as expected: a substantial decrease in Scripting (from 53.9% down to just 14.7% when compared to multiple styled components). In turn, relatively more time is spent on the Rendering routine. In overall, this indicates almost a 7 to 8x total improvement over the two previous approaches.
In conclusion, the benchmark results of the demo app compare as follows:
However, like mentioned earlier, injecting a string of HTML resulted in just a 2-4x increase in performance in our actual application, even though in the demo app the difference is much larger.
This is because the layout and styles in our use case are even more complicated. Therefore most of the time is spent in the rendering routine as opposed to scripting. So in the end, by refactoring, we managed to minimize the time spent on scripting to a fraction of what it used to be, but there was little we could do to speed up the rendering as we couldn't simplify the layout nor remove any of the styles. This was as far as we could go.
Though Fastest, Injecting a String of HTML Is a Step Back
Although replacing components with HTML strings might yield noticeable improvements to rendering performance, the approach comes at a cost: you're no longer able to rely on the virtues of modern front-end frameworks, namely binding values and actions to DOM nodes.
If your components are dynamic, you'll have to revert to old-school ways of managing content and reacting to user interaction. This contributes to overall code complexity, bugginess and development time. Unless the performance of the view in question is absolutely crucial, going for injecting HTML strings is totally not worth it.
Luckily for us, our diff content is static. The only dynamic part we have is the commenting functionality, which includes a few click events that are easy to bind to by listening to clicks on the parent element. Despite generating the content as an HTML string, we managed to keep the comments implemented as traditional Ember components. The approach we took is worth a topic of its own which we will write about in one of the upcoming articles.
Like in everything, there is no silver bullet - every approach has its compromises.
Incremental rendering is the way to go unless you need to support native browser search or are very strict about the look and feel.
The layout and CSS styles of a component have a significant effect on total performance. The overhead of component initialization (the Scripting portion of Chrome performance reports) may lose significance when compared to time spent on rendering.
Replacing multiple components with a single template is beneficial only if your components are tiny and logicless.
Injecting HTML strings directly into the DOM gives a huge performance boost, but if the layout/styling is complicated then most of the time will still be spent on browser rendering. Optimizing browser rendering is very difficult, if at all possible.
Injecting HTML strings is only reasonable when working with static content.
Share your thoughts!
Know better rendering performance optimizations? Found incorrect statements in the article?
Don't hesitate to share your thoughts in the comments below!
This blog post was written in collaboration with Andrey Mikhaylov (lolmaus) who also works at Deveo.
Seamless software development.
Code management and collaboration platform with Git, Subversion, and Mercurial.