3

I have an element I'm applying some basic transitions to based on scroll position. It works smoothly as expected in Safari and Firefox, but scrolling in Chrome is very choppy.

$(document).ready(function($) {
  var divUp = $('.fade');
  var divDown = $('.fade-down');
  $(window).on('scroll', function() {
    var st = $(this).scrollTop();
    divUp.css({ 
        'top'     : -(st/6)+"px", 
        'opacity' : 1 - st/400
    });
    divDown.css({
        'opacity' : 1 - st/400
    }); 
  });
});

I commented out each CSS property individually, but Chrome is choppy either way. The top property is moving a relatively positioned element.

How can I achieve the desired effect while still making Chrome's JS engine happy? Thanks in advance for any feedback.

SGLVEGAS
  • 159
  • 2
  • 8

1 Answers1

19

You're experiencing layout thrashing.

Changing an element's top property invalidates the current layout. Usually this prompts the browser to re-compute the layout asynchronously (i.e. not immediately).

However, calling scrollTop forces the browser to re-layout synchronously. Because you call it in a scroll event handler, this happens repeatedly in a very short space of time. This sequence of DOM write-reads is a known cause of jank.

To improve performance you need to prevent layout thrashing. Changing the CSS transform (and opacity) properties does not invalidate the browser's layout - they only require a composite, which is much faster.

If you animate a transform: translateY instead of top the browser won't need to compute costly calculations on every animation frame:

divUp.css({ 
  'transform': 'translateY( ' + (-(st/6)) + 'px)', 
  'opacity': 1 - st/400
});

You can help the browser optimise for the transition by setting the CSS will-change property:

.your-div {
  will-change: transform;
}

Further reading:

  • Jank free - Articles on improving web app performance
  • CSS Triggers - Lists the steps that browsers need to take when each CSS property is changed
joews
  • 29,767
  • 10
  • 79
  • 91
  • What a fantastic response, so informative - thank you! Using the `transform` property definitely improved performance, however, Safari is no longer transitioning the element. I tried using the `-webkit` vendor prefix but that did not work either. Any suggestions on what this could be? Thanks again for your help! – SGLVEGAS Sep 17 '14 at 16:44
  • Is it working in other browsers? According to [caniuse.com](http://caniuse.com/#search=transform), Safari has always supported `-webkit-transform`, though it is not yet supported without the prefix,. Also, it seems that [jQuery automatically adds prefixes](http://stackoverflow.com/questions/17487716/does-css-automatically-add-vendor-prefixes) so that shouldn't be a problem. Can you make a JSFiddle with a simplified version? – joews Sep 17 '14 at 18:32
  • Ok, I created a crude fiddle to reproduce the issue. http://jsfiddle.net/7uusvfwp/ – SGLVEGAS Sep 17 '14 at 19:12
  • Nice! you've found a way to break webkit :) The good news is that your transition is beautifully smooth everywhere else. – joews Sep 17 '14 at 19:20
  • Yes, and I really appreciate your help! It's for a personal website so I can live with Safari just fading it out but this is very strange. – SGLVEGAS Sep 17 '14 at 19:25
  • Haha, I figured it out. It was a typo in my answer! The `translateY` rule was missing a close bracket. It should be `'translateY( ' + (-(st/6)) + 'px)'`. It looks like the other CSS engines are more tolerant that webkit. – joews Sep 17 '14 at 19:37
  • @joews As I can see, opacity triggers Composite and Paint, Webkit and Edge trigger layout in addition. – Legends Feb 22 '17 at 08:33
  • Why not just recommend: `$('body,html').animate({ scrollTop: 0 },"slow");` Instead of zero, you set what you need. – Legends Feb 22 '17 at 08:42