Comments:" We spent a week making Trello boards load extremely fast. Here’s how we did it. - Fog Creek Blog "
We made a promise with Trello: you can see your entire project in a single glance. That means we can show you all of your cards so you can easily see things like who is doing what, where a task is in the process, and so forth, just by scrolling.
You all make lots of cards. But when the site went to load all of your hundreds and thousands of cards at once, boards were loading pretty slow. Okay, not just pretty slow, painfully slow. If you had a thousand or so cards, it would take seven to eight seconds to completely render. In that time, the browser was totally locked up. You couldn’t click anything. You couldn’t scroll. You just had to sit there.
With the big redesign, one of our goals was to make switching boards really easy. We like to think that we achieved that goal. But when the browser locked up every time you switched boards, it was an awfully slow experience. Who cared if the experience was easy? We had to make it fast.
So I set out on a mission: using a 906 card board on a 1400×1000 pixel window, I wanted to improve board rendering performance by 10% every day for a week. It was bold. It was crazy. Somebody might have said it was impossible. But I proved that theoretical person wrong. We more than achieved that goal. We got perceived rendering time for our big board down to one second.
Naturally, I kept track of my daily progress and implementation details in Trello. Here’s the log.
Monday (7.2 seconds down to 6.7 seconds. 7% reduction.)
Heavy styles like borders, shadows, and gradients can really slow down a browser. So the first thing we tried was removing things like borders on avatars, card borders, backgrounds and borders on card badges, shadows on lists, and the like. It made a big impact, especially for scrolling. We didn’t set out for a flat design. Our primary objective was to make things faster, but the result was a cleaner, simpler look.
Tuesday (6.7 seconds down to 5.9 seconds. 12% reduction.)
On the client, we use backbone.js to structure our app. With backbone, it’s really convenient to use views. Really, very convenient. For every card, we gave each member its own view. When you clicked on a member on a card, it came up with a mini-profile and a menu with an option to remove them from the card. All those extra views generated a lot of useless crap for the browser and used up a bunch of time.
So instead of using views for members, we now just render the avatars and use a generic click handler that looks for a data-idmem
attribute on the element. That’s used to look up the member model to generate the menu view, but only when it’s needed. That made a difference.
I also gutted more CSS.
Wednesday (5.9 seconds… to 5.9 seconds. 0% reduction.)
I tried using the browser’s native innerHtml and getElementByClassName API methods instead of jQuery’s html and append. I thought native APIs might be easier for the browser to optimize and what I read confirmed that. But for whatever reason, it didn’t make much of a difference for Trello.
The rest of the day was a waste. I didn’t make much progress.
Thursday (5.9 seconds down to 960ms)
Thursday was a breakthrough. I tried two major things: preventing layout thrashing and progressive rendering. They both made a huge difference.
Preventing layout thrashing
First, layout thrashing. The browser does two major things when rendering HTML: layouts, which are calculations to determine the dimensions and position of the element, and paints, which make the pixels show up in the right spot with the correct color. Basically. We cut out some of the paints when we removed the heavy styles. There were fewer borders, backgrounds, and other pixels that the browser had to deal with. But we still had an issue with layouts.
Rendering a single card used to work like this. The card basics like the white card frame and card name were inserted into the DOM. Then we inserted the labels, then the members, then the badges, and so on. We did it this way because of another Trello promise: real-time updates. We needed a way to atomically render a section of a card when something changed. For example, when a member was added it triggered the cardView.renderMembers
method so that it only rendered the members and didn’t need to re-render the whole card and cause an annoying flash.
Instead of building all the HTML upfront, inserting it into the DOM and triggering a layout just once; we built some HTML, inserted it into the DOM, triggered a layout, built more HTML, inserted it into the DOM, triggered a layout, built more HTML, and so on. Multiple insertions for each card. Times a thousand. That’s a lot of layouts. Now we render those sections before inserting the card into the DOM, which prevents a bunch of layouts and speeds things up.
In the old way, the card view render function looked something like this…
render: ->
data = model.toJSON()
@$.innerHTML = templates.fill(
'card_in_list',
data
) # add stuff to the DOM, layout
@renderMembers() # add more stuff to the DOM, layout
@renderLabels() # add even more stuff to the DOM, layout
@
With the change, the render function looks something like this…
render: ->
data = model.toJSON()
data.memberData = []
for member in members
memberData.push member.toJSON()
data.labelData = []
for labels in labels when label.isActive
labelData.push label
partials =
"member": templates.member
"label": templates.label
@$.innerHTML = templates.fill(
'card_in_list',
data,
partials
) # only add stuff to the DOM once, only one layout
@
We had more layout problems, though. In the past, the width of the list would adjust to your screen size. So if you had three lists, it would try to fill up as much as the screen as possible. It was a subtle effect. The problem was that when the adjustment happened, the layout of every list and every card would need to be changed, causing major layout thrashing. And it triggered often: when you toggled the sidebar, added a list, resized the window, or whatnot. We tried having lists be a fixed width so we didn’t have to do all the calculations and layouts. It worked well so we kept it. You don’t get the adjustments, but it was a trade-off we were willing to make.
Progressive rendering
Even with all the progress, the browser was still locking up for five seconds. That was unacceptable, even though I technically reached my goal. According to Chrome DevTools’ Timeline, most of the time was being spent in scripts. Trello developer Brett Kiefer had fixed a previous UI lockup by deferring the initialization of jQuery UI droppables until after the board had been painted using the queue method in the async library. In that case, “click … long task … paint” became ”click … paint … long task“.
I wondered if a similar technique could be used for rendering cards progressively. Instead of spending all of the browser’s time generating one huge amount of DOM to insert, we could generate a small amount of DOM, insert it, generate another small amount, insert it, and so forth, so that the browser could free up the UI thread, paint something quickly, and prevent locking up. This really did the trick. Perceived rendering went down to 960ms on my 1,000 card board.
That looks something like this…
Here’s how the code works. Cards in a list are contained in a backbone collection. That collection has its own view. The card collection view render method with the queueing technique looks like this, roughly…
render: ->
renderQueue = new async.queue (models, next) =>
@appendSubviews(@subview(CardView, model) for model in models)
# _.defer, a.k.a. setTimeout(fn, 0), will yield the UI thread
# so the browser can paint.
_.defer next
, 1
chunkSize = 30
models = @getModels()
modelChunks = []
while models.length > 0
modelChunks.push(models.splice(0, chunkSize))
for models in modelChunks
# async.queue flattens arrays so lets wrap this array
# so it’s an array on the other end...
renderQueue.push [models]
@
We could probably just do a for loop with a setTimeout 0 and get the same effect since we know the size of the array. But it worked, so I was happy. There is still some slowness as the cards finish rendering on really big boards, but compared to total browser lock-up, we’ll accept that trade-off.
Trello developer Daniel LeCheminant chipped in by queueing event delegation on cards. Every card has a certain number of events for clicking, dragging, and so forth. It’s more stuff we can put off until later.
We also used the translateZ: 0
hack for a bit of gain. With covers, stickers, and member avatars, cards can have a lot of images. In your CSS, if you apply translateZ: 0
to the image element, you trick the browser into using the GPU to paint it. That frees up the CPU to do one of the many other things it needs to do. This browser behavior could change any day which makes it a hack, but hey, it worked.
Friday
I made a lot of bugs that week, so I fixed them on Friday.
That was the whole week. If rendering on your web client is slow, look for excessive paints and layouts. I highly recommend using Chrome DevTool’s Timeline to help you find trouble areas. If you’re in a situation where you need to render a lot of things at once, look into async.queue or some other progressive rendering.
Now that we have starred boards and fast board switching and rendering, it’s easier than ever to using multiple boards for your project. We wrote “Using Multiple Boards for a Super-Flexible Workflow” on the Trello blog to show you how to do it. On the UserVoice blog, there’s a great article about how they structure their workflow into different boards. Check those out.
If you’ve got questions, I’ll try and answer them on Twitter. Go try out the the latest updates on trello.com. It’s faster, easier, and more beautiful than ever.