Why does Chrome run transform animation on the main thread in some cases, and not in others?
Asked Answered
S

1

38

What is the criteria?

In the following example, I am animating CSS transform, and when you click anywhere (while in Google Chrome) the animation is blocked by a 2-second-long while loop.

Why is the CSS transform animation blocked?

EDIT: Lately Chrome no longer blocks the transform while the main thread is blocked, indicating that they have moved the sort of animation in the following example off main thread.

Animating transform can happen on a separate thread, but it isn't clear exactly when. Sometimes it works.

In this first example, separate-thread transform animation does not happen (click on it to block the main thread and therefore pause the animation):

window.addEventListener('click', kill)

function kill() {
  var start = +new Date;
  while (+new Date - start < 2000){}
}
html, body, div {
        width: 100%; height: 100%;
        margin: 0; padding: 0;
        /* background: #364659; */
        /* background: #293442; */
        background: #1E2630;
        overflow: hidden;
    }

    @keyframes ShimmerEffect {
        0% { transform: translate3d(-15%, -15%, 0) }
        100% { transform: translate3d(-60%, -60%, 0) }
    }
    .shimmerSurface {
/*         overflow: hidden; */
/*         perspective: 100000px */
    }
    .shimmerSurfaceContent {
        transform-style: preserve-3d;
        background: linear-gradient(
            -45deg,
            rgba(0,0,0,0) 40%,
            rgba(244,196,48,0.6) 50%,
            rgba(0,0,0,0) 60%
        );
        background-repeat: repeat;
        background-size: 100% 100%;
        width: 400%; height: 400%;

        animation: ShimmerEffect 1.8s cubic-bezier(0.75, 0.000, 0.25, 1.000) infinite;
    }
<div class="shimmerSurface">
    <div class="shimmerSurfaceContent"></div>
</div>

(codepen link)

EDIT: seems the example's animation is not blocked in Safari (though it chops the gradient), but is blocked only in Chrome and Firefox. How can we unblock the animation in Chrome and Firefox?

In next example, when you click anywhere to block the main thread (in Chrome), you will see that transform is animated on a separate thread because it continues to animate, while the stroke-offset animation is frozen because apparently stroke-offset animation is happening on the main thread:

window.addEventListener('click', kill)

function kill() {
  var start = +new Date;
  while (+new Date - start < 2000){}
}
.loader {
  --path: #2F3545;
  --dot: #5628EE;
  --duration: 3s;
  width: 44px;
  height: 44px;
  position: relative;
}
.loader:before {
  content: "";
  width: 6px;
  height: 6px;
  border-radius: 50%;
  position: absolute;
  display: block;
  background: var(--dot);
  top: 37px;
  left: 19px;
  transform: translate(-18px, -18px);
  -webkit-animation: dotRect var(--duration) cubic-bezier(0.785, 0.135, 0.15, 0.86) infinite;
          animation: dotRect var(--duration) cubic-bezier(0.785, 0.135, 0.15, 0.86) infinite;
}
.loader svg {
  display: block;
  width: 100%;
  height: 100%;
}
.loader svg rect,
.loader svg polygon,
.loader svg circle {
  fill: none;
  stroke: var(--path);
  stroke-width: 10px;
  stroke-linejoin: round;
  stroke-linecap: round;
}
.loader svg polygon {
  stroke-dasharray: 145 76 145 76;
  stroke-dashoffset: 0;
  -webkit-animation: pathTriangle var(--duration) cubic-bezier(0.785, 0.135, 0.15, 0.86) infinite;
          animation: pathTriangle var(--duration) cubic-bezier(0.785, 0.135, 0.15, 0.86) infinite;
}
.loader svg rect {
  stroke-dasharray: 192 64 192 64;
  stroke-dashoffset: 0;
  -webkit-animation: pathRect 3s cubic-bezier(0.785, 0.135, 0.15, 0.86) infinite;
          animation: pathRect 3s cubic-bezier(0.785, 0.135, 0.15, 0.86) infinite;
}
.loader svg circle {
  stroke-dasharray: 150 50 150 50;
  stroke-dashoffset: 75;
  -webkit-animation: pathCircle var(--duration) cubic-bezier(0.785, 0.135, 0.15, 0.86) infinite;
          animation: pathCircle var(--duration) cubic-bezier(0.785, 0.135, 0.15, 0.86) infinite;
}
.loader.triangle {
  width: 48px;
}
.loader.triangle:before {
  left: 21px;
  transform: translate(-10px, -18px);
  -webkit-animation: dotTriangle var(--duration) cubic-bezier(0.785, 0.135, 0.15, 0.86) infinite;
          animation: dotTriangle var(--duration) cubic-bezier(0.785, 0.135, 0.15, 0.86) infinite;
}

@-webkit-keyframes pathTriangle {
  33% {
    stroke-dashoffset: 74;
  }
  66% {
    stroke-dashoffset: 147;
  }
  100% {
    stroke-dashoffset: 221;
  }
}

@keyframes pathTriangle {
  33% {
    stroke-dashoffset: 74;
  }
  66% {
    stroke-dashoffset: 147;
  }
  100% {
    stroke-dashoffset: 221;
  }
}
@-webkit-keyframes dotTriangle {
  33% {
    transform: translate(0, 0);
  }
  66% {
    transform: translate(10px, -18px);
  }
  100% {
    transform: translate(-10px, -18px);
  }
}
@keyframes dotTriangle {
  33% {
    transform: translate(0, 0);
  }
  66% {
    transform: translate(10px, -18px);
  }
  100% {
    transform: translate(-10px, -18px);
  }
}
@-webkit-keyframes pathRect {
  25% {
    stroke-dashoffset: 64;
  }
  50% {
    stroke-dashoffset: 128;
  }
  75% {
    stroke-dashoffset: 192;
  }
  100% {
    stroke-dashoffset: 256;
  }
}
@keyframes pathRect {
  25% {
    stroke-dashoffset: 64;
  }
  50% {
    stroke-dashoffset: 128;
  }
  75% {
    stroke-dashoffset: 192;
  }
  100% {
    stroke-dashoffset: 256;
  }
}
@-webkit-keyframes dotRect {
  25% {
    transform: translate(0, 0);
  }
  50% {
    transform: translate(18px, -18px);
  }
  75% {
    transform: translate(0, -36px);
  }
  100% {
    transform: translate(-18px, -18px);
  }
}
@keyframes dotRect {
  25% {
    transform: translate(0, 0);
  }
  50% {
    transform: translate(18px, -18px);
  }
  75% {
    transform: translate(0, -36px);
  }
  100% {
    transform: translate(-18px, -18px);
  }
}
@-webkit-keyframes pathCircle {
  25% {
    stroke-dashoffset: 125;
  }
  50% {
    stroke-dashoffset: 175;
  }
  75% {
    stroke-dashoffset: 225;
  }
  100% {
    stroke-dashoffset: 275;
  }
}
@keyframes pathCircle {
  25% {
    stroke-dashoffset: 125;
  }
  50% {
    stroke-dashoffset: 175;
  }
  75% {
    stroke-dashoffset: 225;
  }
  100% {
    stroke-dashoffset: 275;
  }
}
.loader {
  display: inline-block;
  margin: 0 16px;
}

html {
  -webkit-font-smoothing: antialiased;
}

* {
  box-sizing: border-box;
}
*:before, *:after {
  box-sizing: border-box;
}

body {
  min-height: 100vh;
  background: #F5F9FF;
  display: flex;
  justify-content: center;
  align-items: center;
}
body .dribbble {
  position: fixed;
  display: block;
  right: 20px;
  bottom: 20px;
}
body .dribbble img {
  display: block;
  height: 28px;
}
<div class="loader">
    <svg viewBox="0 0 80 80">
        <circle id="test" cx="40" cy="40" r="32"></circle>
    </svg>
</div>

<div class="loader triangle">
    <svg viewBox="0 0 86 80">
        <polygon points="43 8 79 72 7 72"></polygon>
    </svg>
</div>

<div class="loader">
    <svg viewBox="0 0 80 80">
        <rect x="8" y="8" width="64" height="64"></rect>
    </svg>
</div>

Why does the first example's transform animation run on the main thread, while the second example's transform animation runs on a separate thread?

What are the criteria under which a transform is guaranteed to run in a separate thread (at least, in Chrome)?

Sector answered 4/4, 2019 at 4:14 Comment(12)
I believe things are opposite: CSS animation does not block main thread. but can be blocked by main thread.Mia
have found thread that says Chrome@Android also works on different wayMia
I'm suspect you are testing it in wrong way. Its much likely browser froze for 2 seconds as a result of executing an infinite loop for 2 seconds.Gayl
@MasoudKeshavarz Yes, the main thread freezes, but in Safari the animation continues running (on another CPU thread) even while the JS is frozen. It only does not work in Chrome or Firefox (although I read a bunch of other places that it should work).Sector
In short, that is because CSS animations take place on the UI thread. On desktop/iOS Safari and Android Chrome, these browsers have since moved CSS animations (compositing) away from using the UI thread, which means that they will not be affected by the 2-second infinity loop invoked by the click event callback.Lackaday
@Lackaday That article says transform runs on a separate thread, not the UI thread. For proof, try clicking on the following example, and only one spinner will freeze (in all browsers): codepen.io/trusktr/pen/mgPOEK The question is, why does my example freeze, if I'm using transform?Sector
@Lackaday I pasted the wrong link in my last comment. In this pen, try clicking anywhere to block the main UI thread, but notice that some animation still runs (the dots move, while the outlines don't): codepen.io/trusktr/pen/xeZLZG In this example, transform in in fact running on a separate thread.Sector
To my understanding, CSS animations take place while the DOM is rendering a new update. The paint method is the last thing that is fired in the event loop, and thus the JS loop that you have causes the DOM to freeze while it finishes the JS. To have more control of the animation you could use requestAnimationFrame(). RAF is called before the DOM is repainted, and thus will have a smoother playback. Check out this video on the JS Event loop.Mok
@JonathanKnoll That will not stop a long-running JS task from blocking rendering, regardless of using requestAnimationFrame or not. This question asks why CSS transforms are only sometimes blocked when JS is stuck in a loop. In my last comment's example, click to run a synchronous JS loop, and you will see that only one of the two animations is frozen, showing that transform runs in a separate thread in that case. The question is, why is that true in that particular example, while transform can be blocked in other cases. (The OP example's transform is lately not blocked in Chrome.)Sector
At the time of this comment your example is no longer blocking animation when clicking the page. This makes the question unanswerable. Voting to close it, since the issue appears to have been fixed.Sorcim
@Sorcim Yeah, I was recently pleasantly surprised that it works now. Yay. Well, here's another example of transforms that don't run on GPU: #30477352 . But it is more about the SVG renderer than the regular CSS renderer.Sector
To better debug transform issues see "Layers" tab in Devtools -> [three dots in the upper right corner] -> More tools -> LayersOxidase
N
3

Browser Threads

Each browser has at least three threads; precisely what is run on each depends on the browser. Modern browsers all have more than three now, but they still have three categories of threads that will always be separate. Why? One will always be entirely separate and only accessible by the browser to handle things like scrolling, opening a new tab etc... At least one will always be for things like calculating and parsing and so will be run on the CPU. And at least one thread will run on the GPU as it is required for something to be shown on your screen.

Layers

For the GPU to know what it's showing on the screen it needs the layout rasterised in a bitmap format. But as things move around the screen it's best if we send the GPU a few bitmaps that can move around. We call these layers.

as @irdkwmnsb has pointed out we can use the layers tab in the developer tools to see exactly which elements have been split into separate bitmaps.

enter image description here

Explicitly Creating A Layer

For any HTML or SVG element that we know will transform, we can add the following CSS rule to ensure the element is separated into a separate bitmap layer and the transition shouldn't be blocked by other activity on the main thread:
will-change: transform

so adding the CSS rule

.shimmerSurfaceContent {
   will-change: transform;
}

should stop the transition from being blocked in your first example.

Why Only In Some Browsers?

The reason some browsers may not automatically split this element into a separate layer is that there is a performance issue with creating too many bitmap layers so they are careful not to create too many. Also, some things don't look good when created as separate bitmaps and moved around so the browser may avoid it.

But for this example specifically, we can see from the two bitmap layers in this image that the top one has a semi-transparent edge. Things like this have previously caused aliasing problems for the GPU as it calculates the various shaded of yellow.
enter image description here This may have been a reason for chrome to previously avoid separating it into a new bitmap layer.

Nonrecognition answered 9/6, 2022 at 18:22 Comment(1)
please provide links to your resources. i've never heard of these threads!!Karikaria

© 2022 - 2024 — McMap. All rights reserved.