89

I use following keyframe animation on several elements:

@keyframes redPulse {
    from { background-color: #bc330d; box-shadow: 0 0 9px #333; }
    50% { background-color: #e33100; box-shadow: 0 0 18px #e33100; }
    to { background-color: #bc330d; box-shadow: 0 0 9px #333; }
}
@-webkit-keyframes redPulse {
    from { background-color: #bc330d; box-shadow: 0 0 9px #333; }
    50% { background-color: #e33100; box-shadow: 0 0 18px #e33100; }
    to { background-color: #bc330d; box-shadow: 0 0 9px #333; }
}
.event_indicator {
    display: inline-block;
    background-color: red;
    width: 5px;
    margin-right: 5px;

    -webkit-animation-name: redPulse;
    -webkit-animation-duration: 1s;
    -webkit-animation-iteration-count: infinite;

    animation-name: redPulse;
    animation-duration: 1s;
    animation-iteration-count: infinite;
}

On my computer I am getting around 40% CPU usage both in Chrome and Firefox. Is it the current state of animations (nice but not usable for now) or am I missing some magic property?

You can check the following sample with the same animation: http://jsfiddle.net/Nrp6Q/

Ilya Tsuryev
  • 2,766
  • 6
  • 25
  • 29
  • 1
    In addition to high CPU, in my case it also seems associated with ever-increasing memory footprint, based on the Chrome Task Manager. – Kevin Bullaughey Mar 22 '16 at 16:44
  • @KevinBullaughey, apparently, every object come with a cost: they take up memory in system RAM and/or on the GPU, see [the explanation](http://stackoverflow.com/a/41797773/4354249), plus animation itself is a relatively expensive operation! – Farside Jan 23 '17 at 00:59
  • 2
    don't animate box-shadow. instead move box-shadow property to pseudoelement and animate it's opacity and transform properties – Denis Apr 05 '17 at 14:29
  • @Denis `move box-shadow property to pseudoelement and animate it's opacity and transform properties` I know the question of the OP is quite old, but could you provide an example or a reference? – tonix Aug 17 '19 at 14:51

7 Answers7

103

Yes, this is normal because you have several infinite-loop animations on the page. The CPU is therefore continually doing work while these elements are rendered. There is a "magic" property that will significantly cut-down the CPU usage and that is:

transform: translateZ(0);

This will composite the elements into their own layers (by tricking the browser into thinking it will be doing 3D transforms) and the browser should, in most cases, take advantage of GPU acceleration, lessening the burden on the CPU. For me this cut it down by about 20% (almost half).

To read more about this technique take a look at: http://ariya.blogspot.com/2011/07/fluid-animation-with-accelerated.html

Additionally, the more keyframes you have in the animation, the more taxing it will be as well. Just try the animation with the middle keyframe cut out and you will see another substantial (~10-12%) drop in CPU usage.

Lastly, not all properties are equal -- box-shadow is much harder for the browser to animate smoothly than, say, background-color. Leaving all of the keyframes intact but dropping the box-shadow property, using the translateZ(0) trick had my CPU usage hovered at only 10-11%.

As much as it pains me to say this, for infinite-loop animations an animated .gif is going to perform much, much better than CSS3 in the current state of browser animation, especially if you plan for many of them to remain rendered on the page for some time.

Update 2017:

For those still finding their way to this question and answer, translate3d(0, 0, 0) provides the same benefit as translateZ(0), you're just also setting translateX() and translateY() at the same time. Please ignore the comment by @Farside as he uses translate3d(X, Y, Z) in his demo but does not compare it to translate(X, Y), which would show that using this technique still makes a significant difference.

According to this question, some people have found better performance across all browsers, especially Chrome, with transform: rotateZ(360deg).

skyline3000
  • 7,639
  • 2
  • 24
  • 33
  • Thank you, that's a pity indeed. We will also fallback to using gifs. In my case `transform` made a huge impact: 40% -> ~20% and with all other changes 16-20%. – Ilya Tsuryev Nov 12 '12 at 14:09
  • 1
    Why downvote when your findings are consistent with what I've written above? You have an infinite loop running in that example - my CPU usage is between 10-14%. You used translate3d which is the same as using translatez(0) - when you remove that the CPU usage goes up. It certainly does still make a difference. – skyline3000 May 01 '16 at 14:58
  • 4
    @Farside please consider editing or deleting your comment as it is definitively wrong and giving misinformation. Your demo does not compare animations with and without `translateZ(0)`, it only shows `translate3d(x, y, z)`. If you compared that to just `translate(x, y)`, you would see the significant difference this method still makes. – skyline3000 Jan 21 '17 at 04:01
  • @skyline3000, if you want to get more information on the topic, why it doesn't work for every case, see [my explicit answer](http://stackoverflow.com/a/41797773/4354249), also if will be helpful to find out modern way of doing the thing. – Farside Jan 23 '17 at 00:49
  • 1
    Thank you! Reduces CPU load from 170% (!) to 13%. Wow. – Zack Katz May 03 '17 at 05:09
  • 1
    @skyline3000 The translateZ method seems doesn't improve the performance on macOS. Here is my example https://stackoverflow.com/questions/47296808/how-to-improve-the-performance-of-svg-animation-on-macos – Winston Nov 20 '17 at 05:03
21

One of the possible ways to reduce the load on CPU, is to use a so called null transform hack, which is often hailed as something of a silver bullet. In many cases it will drastically improve rendering performance in WebKit and Blink browsers like Chrome, Opera and Safari.

Usage of the "Null transform hack" (a hardware compositing mode)

The null transform hack basically does two things:

  1. It switches on the hardware compositing mode (assuming it's supported for the platform)
  2. It creates a new layer with its own backing surface

To "force" a browser, simply add one of these CSS properties to the element:

transform: translateZ(0);

/* or its friend: */
transform: translate3d(0, 0, 0);

When working with 3D transforms, it's good to have these properties as well to improve the performance:

backface-visibility: hidden;
perspective: 1000;

Caveats of the "null transform hack"

Enabling a hardware acceleration in CSS3 for a lot of objects may slow down performance! Apparently, each null 3D transform creates a new layer. However, force-hacking layer creation may not always be the solution to certain performance bottlenecks on a page. Layer creation techniques can boost page speed, but they come with a cost: they take up memory in system RAM and on the GPU. So even if the GPU does a good job, the transfer of many objects might be a problem so that using GPU acceleration might not be worth it. The cite from W3C:

However, setting up the element in a fresh layer is a relatively expensive operation, which can delay the start of a transform animation by a noticeable fraction of a second.

Moving a few big objects has a higher performance, than moving lots of small items when using 3D-acceleration. So they must be used wisely and you need to make sure that hardware-accelerating your operation will really help the performance of your page, and that a performance bottleneck is not being caused by another operation on your page.

Moreover, a GPU is designed specifically for performing the complex mathematical/geometric calculations, and offloading operations onto the GPU can yield massive power consumption. Obviously, when hardware kicks in, so does the battery of the target device.

The modern way: the will-change property

The progress is not standing on the one place... W3C introduced the will-change CSS property. To cut the long story short, the will-change property allows you to inform the browser ahead of time of what kinds of changes you are likely to make to an element, so that it can set up the appropriate optimizations before they're needed.

Here's what they say in the the draft:

The will-change property defined in this specification allows an author to declare ahead-of-time what properties are likely to change in the future, so the UA can set up the appropriate optimizations some time before they’re needed. This way, when the actual change happens, the page updates in a snappy manner.

Using will-change, hinting to the browser about an upcoming transformation can be as simple as adding this rule to the element that you’re expecting to be transformed:

will-change: transform;

When developing for mobile, developers are forced to take the wide array of device constraints into consideration while writing mobile web apps. Browsers are becoming smarter, and sometimes, it's better to leave the decision to the platform itself, instead of overlapping acceleration and forcing the behavior in a hacky-way.

Community
  • 1
  • 1
Farside
  • 9,923
  • 4
  • 47
  • 60
  • It states on the [MDN page](https://developer.mozilla.org/en-US/docs/Web/CSS/will-change) "Don't apply will-change to too many elements." and "Use sparingly." Under the hood, current browser implementations of this property are mostly likely still just compositing the elements. Not only is this feature non-standard (it's still Working Draft), it is also clearly not the "silver bullet" approach you're making it out to be. Additionally, your answer doesn't actually explain anything about the OP's CPU usage nor do you show any comparisons of CPU usage. – skyline3000 Apr 20 '17 at 19:23
  • 3
    @skyline3000, I wrote pros, cons, and caveats. Read more carefully, `will-change` - is not my recommendation, but it's more graceful and modern way of doing of _"hinting to browser about upcoming transformation"_, instead of forcing it. "hailed as something of a silver bullet" - belongs to so called `null transform hack`, and not to the `will-change` property, as you state. I'm fine with your down-vote given, I understand that you may afraid of competition ;) – Farside Apr 23 '17 at 20:42
5

I had a similar case of high CPU usage when animating some elements with CSS3. I was animating the "left"-property of ~7 elements, with some opacity- and shadow-properties used in my whole page. I decided to switch to jQuery.animate, which sadly didn't improve the performance at all. My CPU (i7) was still at ~9-15% while displaying the page, several tricks (translateZ, etc) didn't really improve the performance either - while having my layout messed up (some absolute-positioned elements were involved, ouch!).

Then I stumbled upon this wonderful extension: http://playground.benbarnett.net/jquery-animate-enhanced/

I simply referenced the .js-file, didn't make a single change at the jQuery transitions, and my CPU usage is now 1-2% on the very same page.

My recommendation: when facing CPU issues using CSS3 transitions, switch to jQuery + the animate-enhanced-plugin.

konrad_pe
  • 1,189
  • 11
  • 26
  • 4
    In most cases, lots of projects do not contain jquery as a dependency, so importing an additional bundle will potentially be unacceptable. – Dimitrios Filippou Sep 13 '18 at 10:43
3

You can also use this on any of the following class elements where you want to use GPU instead of CPU

.no-cpu {
    transform: translateZ(0);
    -webkit-transform: translateZ(0);
    -ms-transform: translateZ(0);
}

<element class="event_indicator no-cpu">animation...</element >
user1467439
  • 379
  • 6
  • 13
  • 2
    you forgot the 'will-changes', also you should rename it to 'force-gpu' because 'no-cpu' is very inaccurate and very misleading. –  May 11 '17 at 00:06
1

To a particular case of 'pulsing' background animation, reported here, I've come up with a css+js solution.

In my case the background animation was on background-position property rather than on the background-color, but the principle is the same.

Ok, let's say you have a block with a particular background:

<div class="nice-block">...</div>

Let's style it: (scss)

.nice-block {
  background-color: red;
  //or it can be: background: linear-gradient(45deg, #red, #white, #red);
  //and:          background-size: 600% 600%;

  //the transform and will-change properties
  //are here to only enable GPU
  transform: translateZ(0);
  -webkit-transform: translateZ(0);
  -ms-transform: translateZ(0);
  will-change: transform;

  transition: background-color 5s ease;
  //if you want to add a pulsing effect 
  //to a gradient, see the following two lines:
  // background-position: 0% 50%!important;
  // transition: background-position 5s ease;

  &.animated {
    background-color: white;
    //and in case of gradient animation:
    // background-position: 100% 50%!important;
  }
}

Now it's time to make the effect happen by adding a class 'animated' to the block with some JavaScript:

var bgAnimateTimer;
function animateBg () {
  clearTimeout(bgAnimateTimer);
  bgAnimateTimer = setTimeout(function () {
    clearTimeout(bgAnimateTimer);
    bgAnimateTimer = setTimeout(function () {

      document.querySelector('.nice-block').classList.toggle('animated');

      //jQuery alternative is:
      // $('.nice-block').toggleClass('animated');

      animateBg ();
    }, 5000); //5 seconds for the animation effect
  }, 2500); //2.5 seconds between each animation
}

animateBg ();

This improved performace in my case by ~15 times.

(i) Note to calculate seconds for transition and timeouts correctly both in css and js if you want values different from 5 seconds.

Oleg K
  • 51
  • 6
1

Besides all these answers, I've found another way to optimize it. The answer is simply to change animation Xs inifinite linear to animation Xs inifinite steps(20,end). Or even steps(60,end).

The rationale is that the web is doing very advanced calculation to calculate the animation from one state to another endlessly (and they try to smooth things out). By applying a step function, it cuts a (close to) continuous transition to 20 or 60 different separate states. Theoretically it will provide less smooth result compared with linear, but however, if you get a step function to 60, human eyes could tell no difference among a lot of animations.

I hope this helps!

Eric Aya
  • 69,473
  • 35
  • 181
  • 253
Xha
  • 59
  • 3
  • Indeed, a specified "steps" value reduces CPU a lot. I personally needed a "blinking " effect. So steps(1) was perfect and low CPU usage when blinking multiple items/borders/... on the page – Julesezaar Aug 05 '22 at 08:20
  • I used steps(2) on a blinking effect, and chromium (104) still aniomated it at 60Hz and high cpu load. – Remember Monica Aug 16 '22 at 15:26
-1

Animate endless loops with JS instead of CSS infinite to increase CPU perfomance

Method 1 - Change animation to ''
@keyframes comet4Translate {
  0% {
    transform: translateX(0%) translateY(-50%);
  }

  100% {
    transform: translateX(100%) translateY(100%);
  }
}

@keyframes comet1Opacity {
  0% {
    opacity:1;
  }

  100% {
    opacity:0;
  }
}

#XMLID_640_{
  transform: scale(.8) translateX(-20%) translateY(-20%) translateZ(0);
  animation: comet4Translate 7.5s 3s ease-in-out alternate, comet1Opacity 8s .5s ease-in-out alternate;
  will-change: transform;
  will-change: opacity; 
}
createAnimation = (selector, intervalDelay) => {
    setInterval(() => {
    $(selector).css('animation', 'none')

    setTimeout(() => {
        $(selector).css('animation', '')
    }, 3000)
    }, intervalDelay)
}

// first comet
createAnimation('#XMLID_640_', 16000)



Method 2 - Use transition instead of animation
#XMLID_640_{
  transform: scale(.8) translateX(-20%) translateY(-20%) translateZ(0);
  animation: comet4Translate 7.5s 3s ease-in-out alternate, comet1Opacity 8s .5s ease-in-out infinite alternate;
  will-change: transform;
  will-change: opacity; 
  opacity: 1;
}

#XMLID_640_.animated {
  transform: scale(.8) translateX(150%) translateY(150%) translateZ(0);
  opacity:0;
}
let switcher
const animateViaJs = selector => {
    setInterval(() => {
        if (!switcher) {
        switcher = true
        $(selector).css('transition', 'opacity 3s ease-in-out, transform 4s ease-in-out')
        } else {
        switcher = false
        $(selector).css('transition', 'none')
        }

        document.querySelector(selector).classList.toggle('animated')
    }, 5000)
}

animateViaJs('#XMLID_640_')





Method 3 - AnimeJs
#XMLID_640_{
  will-change: transform;
  will-change: opacity; 
  translateZ: 0;
}
const createAnimeJsLoop = (selector, intervalTimer, cfg) => {
    const tl = anime({ loop:true, targets: selector, easing: 'easeInQuad', ...cfg })

    setInterval(() => {
        tl.pause()
        setTimeout(() => tl.restart(), 5000)
    }, intervalTimer)
}

createAnimeJsLoop('#XMLID_640_', 30000, {
opacity: [
    { value: 1, duration: 0 },
    { value: 0, duration: 2000 }
],
translateX: [
    { value: '-20%', duration: 0 },
    { value: '150%', duration: 3000 }
],
translateY: [
    { value: '-20%', duration: 0  },
    { value: '150%', duration: 3000 }
],
translateZ: 0
delay: 3000
})
CyberT33N
  • 78
  • 5
  • `will-change: transform; will-change: opacity; ` is like saying: use: `color: red; color: blue` and expecting it to be *purple*. – Roko C. Buljan Sep 13 '22 at 22:48