Changing CSS transform on scroll: jerky movement vs. smooth movement

scroll-behavior: smooth safari
smooth scroll
scroll effects css
animate scroll
css scroll events
translate3d css animation examples
css transition move left to right
website not scrolling smoothly

I'm dissatisfied with existing parallax libraries, so I'm trying to write my own. My current one consists of three main classes:

  • ScrollDetector tracks an element's scroll position relative to the screen; it has functions to return a float representing its current position:
    • 0 represents the top edge of the element being at the bottom edge of the viewport
    • 1 represents the bottom edge of the element being at the top edge of the viewport
    • All other positions are interpolated/extrapolated linearly.
  • ScrollAnimation uses a ScrollDetector instance to interpolate arbitrary CSS values on another element, based on the ScrollDetector element.
  • ParallaxativeAnimation extends ScrollAnimation for the special case of a background image that should scroll at a precise factor of the window scroll speed.

My current situation is this:

  • ScrollAnimations using transform: translateY(x) work smoothly.
  • ParallaxativeAnimations using translateY(x) work, but animate jerkily.
  • ParallaxativeAnimations using translate3d(0, x, 0) are jerky, but not as badly.
  • The Rellax library's animations, which use translate3d(0, x, 0), work perfectly smoothly.

You can see the comparison on this pen. (The jerkiness shows up best in Firefox.) My library is on Bitbucket.

I don't know where the problem in my library lies and I don't know how to figure it out. Here is an abridged paste of where the heavy lifting is done while scrolling in the ScrollAnimation class that works smoothly:

getCSSValue(set, scrollPosition) {
    return set.valueFormat.replace(set.substitutionString, ((set.endValue - set.startValue) * scrollPosition + set.startValue).toString() + set.unit)
}

updateCSS() {
    var cssValues = [];

    var scrollPosition = this.scrollDetector.clampedRelativeScrollPosition();

    var length = this.valueSets.length;
    for(var i = 0; i < length; i++) {
        cssValues.push(getCSSValue(valueSets[i], scrollPosition) );
    }

    this.setCSS(cssValues);
    this.ticking = false;
}

requestUpdate() {
    if(!this.ticking) {
        requestAnimationFrame(() => { this.updateCSS(); });
    }

    this.ticking = true;
}

And here's the equivalent in the ParallaxativeAnimation class that is jerky:

updateCSS() {
    var scrollPosition = this.scrollDetector.clampedRelativeScrollPosition();
    var cssValues = [];

    var length = this.valueSets.length;
    for(var i = 0; i < length; i++) {
        var scrollTranslate = -((this.scrollTargetSize - this.valueSets[i].parallaxSize) * scrollPosition);

        cssValues.push(
            this.valueSets[i].valueFormat.replace(this.valueSets[i].substitutionString, scrollTranslate.toString() + 'px')
        );
    }

    this.setCSS(cssValues);
    this.ticking = false;
}

requestUpdate() {
    if(!this.ticking) {
        requestAnimationFrame(() => { this.updateCSS(); });
    }

    this.ticking = true;
}

The math doesn't seem any more complicated, so I can't figure how that's affecting animation performance. I thought the difference might have been my styling on the parallax image, but in the pen above, the Rellax version has the exact same CSS on it, but animates perfectly smoothly. Rellax seems to maybe be doing more complicated math on each frame:

var updatePosition = function(percentage, speed) {
  var value = (speed * (100 * (1 - percentage)));
  return self.options.round ? Math.round(value) : Math.round(value * 100) / 100;
};


//
var update = function() {
  if (setPosition() && pause === false) {
    animate();
  }

  // loop again
  loop(update);
};

// Transform3d on parallax element
var animate = function() {
  for (var i = 0; i < self.elems.length; i++){
    var percentage = ((posY - blocks[i].top + screenY) / (blocks[i].height + screenY));

    // Subtracting initialize value, so element stays in same spot as HTML
    var position = updatePosition(percentage, blocks[i].speed) - blocks[i].base;

    var zindex = blocks[i].zindex;

    // Move that element
    // (Set the new translation and append initial inline transforms.)
    var translate = 'translate3d(0,' + position + 'px,' + zindex + 'px) ' + blocks[i].transform;
    self.elems[i].style[transformProp] = translate;
  }
  self.options.callback(position);
};

The only thing I can really tell from Chrome Developer Tools is that the framerate isn't dipping too far below 60 fps, so maybe it's not that I'm doing too much work each frame, but that I'm doing something mathematically incorrect when I calculate the position?

So I don't know. I'm clearly in way over my head here. I'm sorry to throw a whole library at StackOverflow and say "FIX IT", but if anyone can tell what I'm doing wrong, or tell me how to use Developer Tools to maybe figure out what I'm doing wrong, I'd appreciate it very much.


EDIT

Okay, I've figured out that the most important factor in the jitteriness of the scrolling is the height of the element being translated. I had a miscalculation in my library that was causing the background images to be much taller than they needed to be when my scrollPixelsPerParallaxPixel property was high. I'm in the process of trying to correct that now.

You are able to get a visual performance boost by implementing will-change on elements. It is supported in recent browsers (excluding edge and no IE).

The will-change CSS property hints to browsers how an element is expected to change. Browsers may set up optimizations before an element is actually changed. These kinds of optimizations can increase the responsiveness of a page by doing potentially expensive work before they are actually required.

You can either impelment it like:

function gogoJuice() {
  // The optimizable properties that are going to change
  self.elems[i].style.willChange = 'transform';
}

function sleepNow() {
  self.elems[i].style.willChange = 'auto';
}

Or more basically just in the css on the element which you are changing:

.parallax {
  will-change: transform;
}

This property is intended as a method for authors to let the user-agent know about properties that are likely to change ahead of time. Then the browser can choose to apply any ahead-of-time optimizations required for the property change before the property change actually happens. So it is important to give the the browser some time to actually do the optimizations. Find some way to predict at least slightly ahead of time that something will change, and set will-change then.

Animating Movement Smoothly Using CSS, ScrollAnimation s using transform: translateY(x) work smoothly. ParallaxativeAnimation s using translateY(x) work, but animate jerkily. ParallaxativeAnimation s  Smooth scrolling (the animated change of position within the viewport from the originating link to the destination anchor) can be a nice interaction detail added to a site, giving a polished feel to the experience. If you don’t believe me, look at how many people have responded to the Smooth Scrolling snippet here on CSS-Tricks.

Aside from the calculations, you could try running it asynchronously by using Promise:

await Promise.all([
  loop(update);
]);

just to see if it has a positive impact on the performance.

I'd comment, but I don't have enough reputation yet.

Downsides of Smooth Scrolling, When you are moving an element, what you are changing is a you would use it as part of an actual CSS Transition or Animation to simulate movement. using the CPU tend to be more choppy than ones that use the GPU. 31 Changing CSS transform on scroll: jerky movement vs. smooth movement Nov 15 '17 25 Why does using `arg=None` fix Python's mutable default argument issue? May 20 '12

You want to avoid touching the DOM or changing anything on the screen 😁 If you want to change something on the screen, avoid updating the DOM.

Changing an element attribute will touch the DOM, so it will be slow.

You will get better performance by implementing the animation using pure CSS.

You will get even better performance by implementing the animation using the canvas, while also aplying optimization techniques like calling the canvas API as few times as possible.

Smooth Scrolling, Smooth scrolling has gotten a lot easier. animate the scroll), but after trying it out decided to scope this change just Scrolling to an element in JavaScript is fine, so long as you almost move focus to where you are scrolling. AFAIK in Most Browsers this is configured by some flag in about:config or so … There are four kinds of transforms: 1. Move 2. Scale 3. Rotate 4. Skew In this video, we’ll show you to add each of these transform types to your web designs, whether they’re 2D or 3D.

10 Ways to Minimize Reflows and Improve Performance, Whatever technology you use for smooth scrolling, accessibility is a 1000, function() { // Callback after animation // Must change focus! var  Two finger scrolling is buttery smooth in Google Chrome (and seems alright in Internet Explorer) and most other applications. But, in Microsoft Edge and in the Settings application it's quite jerky. The level of jerkiness is controlled by the Mouse Properties control panel > Wheel > Vertical Scrolling > The following number of lines at a time setting (fewer lines = less jerky scrolling).

Using CSS Transitions to SlideUp and SlideDown, This is one of the reasons you encounter issues such as jerky scrolling and unresponsive interfaces. Similarly, directly applying CSS styles or changing the class may alter the layout. Every frame of the animation will cause a reflow. Moving an element one pixel at a time may look smooth but slower  The ease timing function is so nice, perhaps, because it’s a variant of ease-in-out. That is, the change happens slowly both at the beginning and end, and speeds up only in the middle somewhere. This gives soft edges (metaphorically) to the change and generally feels good. ease vs. ease-in-out.

10 Cool Scroll Animation Wordpress Plugins – Bashooka, CSS Transitions are a nice way to replace jQuery animations with smoother counterparts. The effect is basically that I touch or click on an item and that then a on most phones today, you'll find that the animation is pretty jerky as it to replace effects with CSS animations that render much smoother on  As the script is controlling the movement of the object directly I would be inclined to say that the object should be set to kinematic. If you don't wish to set the object as kinematic then i'd suggest using the AddForce() method of the rigidbody to modify the objects position.

Comments
  • Two things: To do a fair comparison of the two versions of your library you should apply them to identical elements. Comparing the scrolling performance of a heading against the scrolling performance of a large background image isn't a fair comparison. I would like to see both versions operating on a background image. Secondly, I notice that Rellax applies a translate3d() transform to the scrolling element, whereas you apply a translateY(). This could affect performance, since translate3d() will force the browser to offload rendering to the GPU.
  • A follow up on my previous comment: translate3d() doesn't seem to improve the scroll jank. Another thing to consider is how frequently you are measuring elements. I looks like you are calling getBoundingClientRect() every redraw. You should only need to measure the element initially and when a resize has occurred. Ideally the only measurement you should make each redraw is window.scrollY.
  • Thanks for the advice to cache the offset, I've implemented that. The jitteriness, though, is actually being caused by the element being translated being too tall. See my edit. The taller it is, the jitterier it scrolls. I have some faulty math causing those parallax backgrounds to be too tall sometimes. My first attempted fix just now made it too tall at different times; still working on the correct algorithm for calculating that height.
  • You're not changing the height of the image mid-scroll though are you? On another note, and you probably know this already, but scroll jank is very common when the element being scrolled has any position value other than fixed. IMO that is the #1 factor that makes parallax so tricky, since fixing the element to the viewport causes all kinds of other layout headaches.
  • Unless I'm doing something very wrong by accident, I'm only updating the image's height on resize. And yeah, I tried doing position: fixed, and it would have worked if that didn't make a new stacking context no longer masked by its parent's being overflow: hidden.
  • The DOM is just an interface into the browser backend. Changing the CSS transform on elements with their own stacked/composite layer bypasses the most expensive parts of the rendering loop and jumps straight to composition. In theory, it should be one of the fastest ways to update an element.