"will-change" must change? Animators beware.

September 28th 2016 | Carl
favorite

34047

Chrome 53 debuted a new "feature" to improve animation performance and graphics fidelity, but it had some nasty side effects that caused quite a few animators to get unpleasant phone calls from angry clients whose ads and web sites suddenly looked blurry and/or stuttery. Every other browser (including previous versions of Chrome) render the same animation beautifully. Chrome's new behavior may also result in WORSE animation performance. A lengthy discussion with the Chrome team revealed some disturbing tradeoffs that animators need to know about, and that could spell trouble with other browsers too unless we band together as a community and make our voices heard. At the heart of the controversy is the will-change CSS property.

What is "will-change"?

It gives developers a way to say "hey, browser, I'm gonna animate this property, so please do whatever you can to prepare and make it happen smoothly" which often means creating a compositor layer to get GPU-acceleration of transforms and opacity. Think of a compositor layer like a screen-shot of the element that the browser can store on the GPU to move/scale/rotate cheaply instead of re-computing all the pixels on each screen refresh. Read Sara Soueidan's excellent in-depth article here for details.

Problem: blurry, stuttering animations

What is Chrome 53 doing differently?

Chrome is basically saying "In the past, I intelligently managed when and how to rasterize elements, but now that will-change property exists, I'll just make that serve as a blind toggle switch for rasterization instead. If things look blurry, it's not my problem - the developer will need to jump through some hoops (described below) to make things look sharp again."

It all boils down to how and when "rasterization" of an element occurs (changing it into pixels stored on the GPU). If rasterization happens when the element is very small, it will be lower resolution. When scaled up, it'll look blurry/pixelated. On the other hand, if you rasterize while it's at its native scale (1) or above, you'll get a much higher-quality image with more pixels.

Another key factor is how it is rasterized.

Apparently Chrome uses a completely different algorithm for rasterizing an <img> than a <div> with a background-image even though both use identical source files and are sized the same! Here's an example of how they look in Chrome 53:

Update: The Chrome team says they've fixed the bug that caused background-image to render differently than <img> (issue 649046) and it should be in the 9/29 release of Chrome Canary.

Both factors (the when and the how) are at play in Chrome 53's new behavior (which apparently was a purposeful engineering choice based on will-change).

According to this document, all content now gets re-rasterized when its scale changes (which happens up to 60 times per second during animation of scale/scaleX/scaleY properties). That's supposed to keep things sharp, but in this case Chrome's background-image algorithm applies some sort of pixel-snapping which causes that odd vibration during animation. (Update: should be fixed soon).

This re-rasterization comes at a cost performance-wise too. Previously, Chrome applied some heuristics to sense when it was appropriate to re-rasterize to avoid blurriness, thus it only kicked in when necessary. But now, you must opt-in to get the layerizing benefits by setting will-change: transform. The Chrome team says this is an "improvement" because it puts the control into the hands of developers, but clearly this shift in behavior comes at quite a price.

Overnight, the rug got pulled out from under many animations around the web, hurting performance (due to the constant re-rasterizing by default) and also introducing those visual vibrations when scaling background-image graphics (quite common for sprite sheets). To be clear, this primarily affected scaling animations, not ALL animations on the web. And of course anything where a background-image was used and the element was layerized (making it blurry in Chrome 53).

Partial solution: set will-change: transform

Complaints rolled in quickly, and the Chrome team suggested that developers go back and edit all their affected animations by adding will-change: transform which would layerize/rasterize those elements (skipping re-rasterizing on every refresh). It's a bit of a nightmare to go back and find all the affected animations and make the necessary edits, but hey, it's just adding one property to the CSS so it shouldn't be too bad, right?

Oops, that breaks it in other ways

Chrome chose to implement will-change such that it will trigger rasterization at whatever the current scale happens to be, so if you've got an element that starts at scale(0.1) and animates up to scale(1), rasterization happens at scale(0.1), thus it will look terrible (blurry/pixelated) at the end of the animation. Here's an example showing the SAME image animating to identical scales, but flip-flopped starting/ending values:

(View the codepen here)

Partial solution: toggle will-change back and forth

Hold your nose...here comes the hack. In order to trigger re-rasterization of the element to keep it clear, the Chrome team suggested toggling will-change back to auto during the animation, then waiting until a requestAnimationFrame elapses before setting it back to transform...and then doing it again, and again, at whatever frequency the developer wants in order to keep things acceptably sharp. So will-change: auto is being pressed into service to explicitly tell the browser "rasterize me on the next requestAnimationFrame."

Yes, you read that correctly: animators must turn **off** the very property whose entire purpose is to be **on** for animation, signaling change. Then toggle it back-and-forth many times during the animation. So we're essentially telling the browser "I'm gonna change this...no I'm not...yes I am...nope..." all while in the process of animating.

This doesn't exactly sound consistent with the intent of will-change, nor does it seem performant (Google's document says "Be aware, however, that there is often a large one-time performance cost to adding or removing will-change: transform.")

Gotcha: never de-layerize

Even if you're willing to follow the advice to set will-change: transform and toggle back-and-forth during the animation to maintain a reasonable level of clarity, there's one last gotcha - if you set it back to will-change: auto at the end of the animation and give it a non-3d transform (to de-layerize it), you'll see a jarring shift in pixels and clarity for anything with a background-image! The Chrome team advises in that case that you make sure it always has a 3d transform thereafter to prevent it from de-layerizing. Following that advice puts you at risk of running out of memory (or hitting performance problems), plus the background-image will always be slightly blurry. Here's what it looks like to toggle between the two modes at the end of the animation:

Update: The Chrome team clarified that this was a temporary fix, not a long-term solution. The background-image rendering bug should be resolved soon, and this "never de-layerize" suggestion will no longer apply at that point.

The bigger issue, beyond Chrome...

The will-change spec doesn't really specify implementation details which means that Chrome's new behavior may be completely unique; Firefox might do something different, and then there's Edge, Safari, Opera, Android, etc. Perhaps Chrome requires that developers toggle back-and-forth to maintain clarity, but what if Firefox interprets that differently, or imposes a big performance penalty when doing the same thing? What if developers must resort to various [potentially conflicting] hacks for each browser, bloating their code and causing all sorts of headaches. We may have to resort to user agent sniffing again (did you just throw up a little in your mouth?). This will-change property that was intended to SOLVE problems for animators may end up doing the opposite.

It seems wise for the browsers to step back and let the spec authors fill in the implementation details and gain consensus before moving forward.

Another problem: stacking contexts

As mentioned in this article, will-change can also affect the stacking context of elements, leading to unintended changes in how things render/stack on your page. So your content may stack differently in browsers that do support will-change than those that don't. More sniffing, yay!

To summarize:

Before Chrome 53

  • Just animate stuff. No need to jump through any hoops as a developer to get decent clarity (though the Chrome team points out that there was still some blurriness in certain edge cases that they've heard complaints about).

After Chrome 53

  • Make sure to set will-change: transform if you're animating transforms and want to opt-in to performance optimizations and avoid jittery background-image scaling. But be careful about how it might affect stacking contexts, and keep checking when other browsers decide to implement will-change, as it could change how your content looks.
  • If you're scaling up, make sure you toggle will-change back-and-forth to/from auto during the animation to maintain clarity (but sacrifice performance).
  • Make sure there's a 3D transform applied throughout to prevent de-layerization (which would cause a big performance hit).
  • Don't switch back to a 2D transform at the end (at least for elements with a background-image), or you'll see a jarring pixel shift. (Update: should be fixed soon in Chrome, so this step won't be necessary at some point.)
  • Don't forget to go back and find/fix existing animations that are affected by the new Chrome behavior.

Is Chrome going to fix this?

As of today, the Chrome team says they've thought a lot about this and feel pretty strongly that the new behavior is an improvement, so there's no plan to change it (that we know of at least).

Here are the solutions we proposed (with the answers we got):

  • Instead of putting the burden on developers to manually toggle will-change back-and-forth between transform and auto during the animation, just have the browser natively sense when re-rasterization is prudent and do that automatically which would be much faster than JS anyway. It seems rather trivial for the browser to sense when an element has been scaled greater than a certain delta (like 0.2) and trigger a rasterization.
    Chrome team's answer (summary): "that's too hard (complex). The browser might get it wrong sometimes, so it's better to have developers do it at the JS level. And JS isn't that much slower than native. It's just a small amount of extra work for animators (or library developers)."
  • The browser could always perform rasterization at native size (scale of 1) or the current scale, whichever is bigger. That way, nobody would run into those nasty blurred images when scaling up from a small value to 1 (pretty common) and there's no need to keep re-rasterizing.
    Chrome team's answer: "rastering at native size and then scaling that down with bilinear filters in the compositor to something less than 0.5 or so will start to loop noticeably bad."
  • If the goal is to put control into developers' hands, why not expose an API for defining what scale rasterization should occur at, like element.cacheTransform = "scale(1)"?
    Chrome team's answer: "it may not be so easy to get it right in terms of expressiveness, and requires new APIs...and also to define what raster scale means, which seems quite tricky in general, especially while not over-fitting to current implementation strategies. That might happen later on."
  • The browser should use the same algorithm to rasterize (and scale) anything. The one being used by Chrome for <img> looks great so please use that for background-image too
    Chrome team's answer (summary): "Acknowledged. We're working on a fix."
  • Instead of turning will-change into a convoluted way to control rasterization in the browser and risk opening a can of worms with other browsers doing things completely differently due to vague specs, roll back the behavior and work with the spec authors to define implementation details and re-approach later when consensus is reached.
    Chrome team's answer: "the intention of will-change is to give a hint to the browser that the referenced property is going to be animated, and for the browser to take steps to optimize performance for that use case. This is why will-change: transform creates a composited layer: because animating transform afterward will therefore not later have the startup cost and per-frame of creating the composited layer and rasterizing it. Following this logic, further extending the meaning of will-change: transform to not re-raster on scale change is similar, because it will make it faster."

Can GSAP fix it for me?

Yes and no. We've already experimented with the suggestions that the Chrome team made, but there are a few tricky challenges. First, we're hyper-focused on performance so it's quite painful to have this new Chrome behavior force us to add extra logic that must run on every tick of every tween of any transform-related animation. It probably wouldn't be noticeable unless you're animating hundreds or thousands of elements simultaneously, but we built GSAP to handle crazy amounts of stress because sometimes that's what a project requires, so we're pretty frustrated by Chrome's decision to impose this burden on animators and libraries like GSAP. But yes, we could do the toggling under the hood automatically and accept the performance tradeoff. But another major problem that's totally in the hands of browsers is rendering - we can't fix that jarring pixel shift at the end of the animations of background-image. We can force the 3D transform to remain, but as described above, that leaves things blurry and unnecessarily eats up memory. We work very hard to implement workarounds for browser inconsistencies and bugs like this, but we can't work miracles. We really need Chrome to step up and provide some better solutions.

Conclusion

There's no doubt that the Chrome team is working hard to move the web forward and deliver the best experience for their users. At GreenSock, Chrome is our primary browser that we use every day, so we're big fans. This article isn't intended to criticize anyone, but rather to bring attention to something that could spell big trouble for animators in the days ahead, beyond the headaches Chrome 53 caused with its new behavior. Hopefully Chrome will roll back the changes and/or implement some of the suggestions above. We'd encourage folks to make their voices heard (on the Chrome thread, below in the comments, with the spec authors, etc.). Perhaps we got something wrong - feel free to correct us or make other suggestions. Ultimately we want to help move animation forward on the web, so please join us.


Get GSAP

Version: 1.20.3 updated 2017-10-02

Core

    Extras

      Plugins

        By using GreenSock code, you agree to the terms of use.

        Help support GreenSock by becoming a member

        Join Club GreenSock