A couple of days ago a colleague of mine noticed that this site was quite sluggish during scroll on IE and Windows Phone. At first we thought this was due to a JavaScript event on scroll or mousewheel but the issue was clearly visible when disabling all JavaScript on the site. This was clearly an CSS related issue and after further debugging we noticed, surprisingly, that the whole page was repainted when scrolling. This post will guide you through how to debug paint bottlenecks and how I solved mine on this site.

Debugging paint times

We'll start with the Timeline tab in Chrome DevTools. This view is really a lifesaver when trying to debug bottlenecks in your website. It produces a waterfall view on the following records:

  • Loading events
  • Scripting
  • Rendering
  • Painting

Further more it has three different modes:

  • Events
  • Frames
  • Memory

In this case we're only interested in the painting records and we'll run it in the frames mode to get an insight into our rendering performance.

Timeline example
Timeline capture when scrolling on this site.

The goal is for your site to always run at 60 fps, e.g. we want the records to be below the 60 fps line. In the above example we see that we have records that go far above, which is a problem.

Identifying the problem

One way to identify paint bottlenecks is to capture timelines of specific actions on the site and then review/compare them. This option is valid in some cases but many times I find it tedious and confusing. I prefer working with an active timeline capture and having Show paint rectangles enabled.

Show paint rectangles example

Show paint rectangles gives you a live graphical indicator on what's being painted on the site:

Paint rectangle example

As you can see, in the example above, DevTools displays a paint rectangle on our link when we hover over it - because it changes color on hover. The combination of these tools are great because it enables us to view how long something takes to be painted but also what that painted item is.

There is also another option and that's enabling Enable continuous page repainting.

Enable continuous page repainting example

This repaints your page over and over and gives you the current page paint time, the lowest time and the highest time in milliseconds. There's also a history with the latest 80 page paint times.

Repainting over and over enables us to see if changes when we remove certain DOM elements. You can either edit the CSS of an element with display: none; or press H (hotkey to hide element in DevTools) to see if the page paint time decreases.

How I solved the paint issue with this site

Before debugging we had already narrowed the issue down to being scroll related. After debugging with the different options mentioned above we saw that the page repainted on scroll, which is far from optimal.

I then began to disable different CSS modules to narrow it down further and after a while I located the problematic module. The problem was the position style on our content area. It was a bit of a mess with a mixture of relative, absolute and fixed. The reason for positioning it that way was for the side menu to work correctly and that the content area would stay in the same place. I had this problem when I opened the menu the content area would scroll straight back to the top.

After trying a couple of different approaches I finally decided that I would remove the positioning styling on the content area when it wasn't opened. This left me the issue of “content area scroll straight to top” which I had to solve in JavaScript.

# Open the menu view.
open: ->  
  # Save scroll position for later.
  _scrollPosition = $c.document.scrollTop()

  # Adjust the site wrapper.
    top: "-#{_scrollPosition}px"
    width: $c.document.width()

  # Open the menu.
  $c.body.addClass "menu-open menu-transition"

The above is my method to open the menu view. The $c.site is our content area and the only this I changed was saving the _scrollPosition and then setting the top and width on our content area. Notice that we set a negative top value, this is the give the impression that the scroll is still in the same place as before.

Now we handle the opening of the menu, now we just have to handle the closing of the menu. Because we're using transitions on the side menu we actually have to do this on the transitionEnd event.

# Bind the transition end events.
_bindTransitionEnd = ->  
  $c.siteContainer.on _transitionEnd, =>
    unless @isOpen()
      if $c.body.hasClass "menu-transition"
        $c.body.removeClass "menu-transition"

        if _scrollPosition?
          # Reset the site wrapper.
            top: "0"
            width: "100%"

          # Scroll to the saved scroll position.
          $c.document.scrollTop _scrollPosition

          # Clear scroll position.
          _scrollPosition = null

The only thing we have to do is to reset our content area, scroll to the saved scroll position from when we opened the menu and lastly clear our saved _scrollPosition.

This implementation solved my paint issues and the site does not paint on scroll anymore. One should really be a bit careful when using position styling, or at least debug it well so you're certain that the site still performs well.

If you want to read more about debugging in DevTools I recommend visiting Chrome Developer which is a great resource.