We were recently asked by the team at eQuorum to investigate an issue with dgrid performance when leveraging the dgrid/Tree
mixin. The issue was challenging to solve, so we thought it would be useful to share our story in debugging and solving it.
Background
dgrid is a powerful Dojo-based component for managing large data sets through a virtual rendering technique. Its primary OnDemandList/OnDemandGrid
APIs use dstore as the source of their data. To optimize performance, only a fraction of the data from the store is rendered at any one time. Rows of data are added as the user scrolls the grid, while some rows of data are pruned to reduce memory consumption.
The dgrid dgrid/Tree
mixin creates a parent-child relationship between rows. The Tree mixin manages the parent-child relationship, but from the OnDemandList
point of view, the grid is still a flat collection of rows.
The issue
When a dgrid was created with a large number of child rows and the parent row expanded to show the child rows, scrolling resulted in the browser slowing down, impacting end user performance and usability. This issue was initially reported nearly two years ago, and something we had been planning to address with dgrid 2. However, the issue became increasingly urgent.
When a parent row was expanded, the child rows were being incrementally added just like any other rows in an OnDemandList
. But the initial row pruning code was not created with the Tree mixin in mind, and was not pruning child rows as the user scrolled those rows far away from the viewport. So with a large number of children, the grid size continued to increase. Even worse, a parent row with a large number of children would cause all row pruning to stop. So scrolling down past the expanded row caused the grid to continue to grow in size.
Reading the original ticket, the description does not necessarily imply that child row pruning was the problem, because the issue describes expanding a row, and then the parent rows not being pruned.
So a basic solution to the reported problem would be to just fix the parent pruning problem. The pruning code could have been improved to fix the large expanded row problem. However, just fixing this problem would mean that all of the child rows would have still been loaded and retained (not-pruned) in the grid.
Beyond parent row pruning
Once the parent row pruning issue was fixed, it was time to solve the deeper challenges. dgrid inserts invisible rows in the grid that act as sentinels (preload nodes). Those sentinels help dgrid decide which rows need to be added and which rows may be pruned. There is one placed before the first row in the grid and one right after the last row in the grid. When using the dgrid/Tree
mixin, a sentinel is inserted at the top and the bottom of each group of child rows. The code that prunes rows was not navigating those sets of sentinels efficiently which caused all pruning to stop.
Rather than just fix the parent row pruning problem, we also wanted to tackle pruning of child rows. In general, we try to make dgrid efficient in all ways which includes running only the code that is needed. If you don’t use the dgrid/Tree
mixin, we don’t want tree related code to run. It can be difficult writing dgrid extensions in this manner, and the dgrid/Tree
mixin is no exception.
The original OnDemandList
was not written with multiple sets of sentinels in mind. The dgrid/Tree
mixin did the best it could but stopped short of child row pruning because that code in OnDemandList
could not be overwritten by the dgrid/Tree
mixin without significant code duplication. The code that manages the sentinels in OnDemandList
needed to be refactored to support multiple sets of sentinels.
OnDemandList
manages the sentinels in a way that the groups of sentinels may have a hierarchy and be nested or the sentinels may be siblings at the same level. The dgrid/Tree
mixin manages the sentinels in a way that defines a tree. A future plugin could use sentinels in a different manner, so that needed to be considered for any solution.
Challenges
As we worked through the solution, we had many challenges to solve. Smooth, slow scrolling is the easiest scenario to handle, where all rows are loaded one at a time, sequentially and the rows are pruned in small increments.
Fast scrolling is where the real challenges occur. If the user scrolls quickly, dgrid recognizes that and rather than try to load every row up to the new scroll position, it makes a jump in the data set. For example, suppose a grid was constructed and initially loaded 50 rows of data. The user then drags the scrollbar down to row 500 of the dgrid. For a jump that large, OnDemandList
will prune all of the currently rendered rows and ask the store for items starting at approximately row 450 to render data for slow scrolling just above the currently selected row. With the dgrid/Tree
mixin and expanded rows, the code has to not only figure out which parent rows might be visible, but it also has to figure out which child rows might be visible. It needs to do this while requesting the smallest number of items from the store and rendering only the rows that are absolutely needed.
In the case of a smooth, consistent scrollbar, as the user scrolls an OnDemandList
, the sentinels grow and shrink in height. This is done so the grid’s content stays a consistent height, which causes the scrollbar’s dimensions to remain constant. As OnDemandList
adds and prune rows, the sentinels’ heights need to change to maintain that overall grid content height. With the dgrid/Tree
mixin being used and multiple rows expanded or even multiple depths of rows expanded (child of a child of a parent kind of thing), there are many sentinel height calculations needed to keep a consistent scrollbar.
Solution
The solution for this fix is primarily contained in two commits:
Thanks!
Thanks also to eQuorum for their generous sponsorship of these efforts and for their contribution to the open source community!
eQuorum focuses on solving complex document management problems for hundreds of organizations in the manufacturing, engineering, utilities and AECO (Architectural, Engineering, Construction and Owner) arenas where customers seek to better organize documents, save time and enhance collaboration.