GitKraken is a great Git GUI for Windows, Mac, and Linux. There’s a bunch of different reasons why, but first, let’s talk about data.
GitKraken is used to navigate a lot of data in a lot of different ways. One of my recent goals was to figure out how to get a bunch of data viewable to the user. So, I started work on Rickscroll, a high-performance scrolling utility for React.
There’s the left panel, graph view, diff view, blame view (plus accompanying commit list), and the commit/WIP browser, all displaying a decent-to-gigantic amount of data in a list-like structure.
And, we need these areas of the application to display their data in a performant fashion. Trouble is, the DOM is slow.
There seems to be a linear relationship between the number of rows to render and time. For small, simple lists, this render time is totally acceptable; however, it’s not acceptable performance for any of the aforementioned views that we have in GitKraken.
The Almighty Left Panel
In order to demonstrate, let’s walk through a scenario that existed in GitKraken in the left panel prior to v1.7.
GitKraken performs an auto-fetch once a minute (with factory settings). When GitKraken performs this auto-fetch, the app finds out that a new branch was added to one of the remotes that GitKraken has listed in the left panel for a given repository.
Let’s imagine we currently have 300 tags, 10 remotes, 90 branches listed as remote, 10 local branches, some pull requests, and last but not least, a stash.
Well, GitKraken passes that new branch entry down into the left panel, which means that we need to do this expensive DOM calculation on far more than just the single row that we want to modify. We really need to re-render quite a bit, as can be seen in the figure above.
The kicker here is that in an auto-fetch, we do a series of fetches against every remote listed on your left panel. Every time a branch is discovered or a reference is updated, the left panel must perform this expensive render.
For these critical areas of the app, we need to be much smarter about how we render our content so that the overall time for a given render is much smaller. We would prefer that each critical section of the app take no more than 10-20ms when responding to a change.
It’s important that we minimize these render times so that the app feels consistently responsive.
The GitKraken dev team had been floating around some high-performance scrolling ideas for quite some time. The graph is currently powered by our first foray into the area. The team’s first version of high-performance scrolling abstracted the scrolling concept into a React component made of three primary divs, the content, and scroll bars.
The content div represented the viewable area of the scrollable, and the two other divs were placeholders that ran the height and width of the viewable content.
Inside the scroll bar divs, we placed a div of the height and width of the viewable content plus any overflow, such that those scroll bar divs would host a native scrollbar. We then hijacked the scroll events for each div and calculated an offset based on every scroll event.
The problem with this component is that it left the child component completely in charge of taking that offset and applying it in a meaningful way.
Our graph was the first component to use the scrollable component and was built with a tiling system to display tiles of content as you scroll. GitKraken shows 150 graph rows per tile. When a user scrolls, GitKraken will render the next tile as it comes into view.
We do the scrolling by rendering every tile div that will exist for the current graph as empty. Each of these divs has a transform property on them that uses Translate3D to move or scroll these divs in the content area.
As we move these blank tiles into view, we populate them with content. Our team debounced the scrolling operation because it turns out that rendering 150 graph rows is still quite an expensive operation (causing a boatload of lag during a typical scroll).
Consequently, if you scroll too quickly, the debounce is the reason that you’ll see blank tiles as you scroll in the graph panel.
At the time, Scrollable was a huge improvement and allowed GitKraken to scale to where it currently is today. The downside is that for lack of a standard tiling solution, each component we scrollabalized reimplemented tiling in a way that made sense to the content that it needed to display.
The diff view, WIP and commit area, graph view, and blame view all ended up building their own tiling solution based on their specific needs — a whole mess of special snowflakes. Scrollable has grown poorly. Working with components wrapped in a Scrollable is very messy.
Rickscroll aims to heal the pain points that we had working with Scrollable. The important points of our various tiling solutions were to separate content into rows and tiles accordingly, to use Translate3D to simulate scrolling, and to try to limit the amount of renderable content to the visible area of the scrollable window.
At this point, we needed to solve the problem of building a clean interface for Rickscroll that addressed all of our application’s needs. Our additional needs were variable height rows, overridable horizontal scrolling, resizable gutters on the left and right, and additional scroll behaviors like locking certain rows to the top of the viewable area as a user scrolls.
It’s also important to note that Translate3D is an important part of this component workflow. Elements marked with Translate3D in their transform property use hardware acceleration. Abusing the hardware acceleration is another way to minimize pains in the DOM during scrolling operations.
What we came up with is an object structure which represents rows in a list. Each row has its own component class (a React component), props, height, and gutter config.
Rickscroll then takes this list, does a single iteration over it, and extracts a mapping of rows to tiles and the corresponding size of those tiles (due to the height property on each row).
Rickscroll is able to map offsets to the start of a tile, and by knowing the size of those tiles, is able to recognize when a tile should be removed from the DOM.
When Rickscroll finishes that once over, it uses the component state to track the offset of the scrollbars and applies that offset to our tile offset map to figure out which tile we should render at the top of the viewable content area.
We then use the calculated height of the viewable area and the sizes of the tiles to figure out how many tiles we need to be showing in order for the visible area to stay populated during any scroll operation.
When we have completed a full translation equal to or greater than the top tile’s size, we remove that tile and we add a new tile to the bottom of the visible area, starting our translation over again from zero. We are able to minimize pains using this flow.
As such, we have provided the ability to pass-through offsets to content components in the rows. This will improve the capacity for rows to behave differently from horizontal scroll offsets (such as collecting graph nodes in the gutter).
Another feature we were able to put into Rickscroll was the handling of sections or multiple lists per single scrollable. By building an additional object around our lists of rows, we can provide a special header row for each of these lists.
That header row can then be factored into our scroll calculations such that we can build useful context for scroll operations, such as locking headers to the top of the scroll window. We achieve this by both inserting the header row into its appropriate position in the list, and by keeping a separate container which hosts a special locked header row.
When Rickscroll determines that a header row is being scrolled toward that special locked header row, we are able to translate the locked header in sync with the movement of the rest of the rows and to replace the content of that special header once we’ve scrolled the new header into the appropriate position.
In GitKraken v1.8, we’ve shipped the left panel with locking headers and sections in Rickscroll.
Now, was it mentioned that we packed all of these nifty features into Rickscroll, and also achieved incredible performance?
There’s still some room to optimize further, but for this level of performance, GitKraken should be able to fly fast as we roll out improvements to our scrollable renders.
When a component needs scrollable content as a series of rows, tiling those rows in small, easily renderable chunks and using hardware acceleration via Translate3D to scroll is a winning outcome.
With these three tricks, we can build all sorts of API and scrolling niceties with little worry of ruining the performance of the application overall. Further, we’re able to move past the scrolling problem, because we now have a suitable API to leverage whenever we need high-performance scrolling in the application.
No more reimplementing tiles. Instead, we’re able to quickly iterate on how a view should work and function.
And, if all else fails, check out this documentation that should help you with Rickscroll.