Jump to content
GreenSock

Search In
  • More options...
Find results that contain...
Find results in...
RolandSoos

Tell GSAP to discard previously applied transforms

Recommended Posts

I'm trying to optimize things in my application. I use the tool in Chrome Dev tools -> Performance -> Record

What I see that I have a fromTo GSAP animation which animates only one property for example "x". At the start of the animations GSAP reads the previously applied transform probably to maintain other at the original value, but this cause a recalculate style in special circumstances. I would like to tell GSAP that it does not need to read the previously set transforms, just overwrite every transform value with the one set in this tween.

 

Do you have any suggestion what I could use?

 

Here is an example what I used:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>CodePen - GSAP Basic fromTo Tween</title>
    <style>
        .box {
            width: 200px;
            height: 200px;
            background: red;
        }
    </style>
</head>
<body>
<div class="box"></div>
<script src='https://s3-us-west-2.amazonaws.com/s.cdpn.io/16327/gsap-latest-beta.min.js'></script>
<script>
    setTimeout(function () {
        const el = document.querySelector('.box');
        
        el.style.left = '10px'; // Force recalculate style

        console.time("FromTo");
        gsap.fromTo(
            el,
            {
                x: 0
            },
            {
                duration: 5,
                x: 500
            }
        );
        console.timeEnd("FromTo");
    }, 2000);
</script>

</body>
</html>

This is how it looks in dev tools:

image.png.b28555e2d942fe665193f578c3e28d81.png

 

In my real world application these recalculations affect the measured performance highly. If I would be able to prevent getComputedStyle() when fromTo tweens are used, I would be able to reduce the time spent with animation initializing about 50%

image.thumb.png.96a0ad1cfe5833b0a1a9eea94fe61b16.png

Link to comment
Share on other sites

The first time you have GSAP handle anything related to transforms on an element, it MUST parse the transforms to segregate them into their individual components properly (x, y, rotation, skew, scale, etc.) and it caches those so that future parsing is skipped and it can read those values super-fast. And again, that only happens the first time you do something transform-related on a particular element. If you're trying to optimize things and you're dealing with a LOT of elements, I suppose you could front-load those tasks by doing a gsap.set() with something transform-related, like gsap.set(".your-class", {x: "+=0"})

 

You only see the _parseTransform() call on the initial action, right? 

  • Like 2
Link to comment
Share on other sites

Also, if you're trying to optimize things, it's counter-productive to do a .fromTo() when your initial value is 0 (or the default). Keep in mind that the "from" part of a .fromTo() is just a zero-duration tween itself, so if you simply do a .to() instead, you'll get better performance. Only use a .fromTo() when you need to force the initial value to be something different than it would normally be at the start of the tween. 

  • Like 2
Link to comment
Share on other sites

Yes, this happens only on the first change. But if you have an animation on page load, it is important to be performant and there is no space for not-needed processing.

 

It is good to know that .fromTo() is slower than .to(). I had an assumption that .fromTo() should be faster as GSAP do not need to read anything from the element itself. All the data available locally.

 

The following code contains the suggested gsap.set() before tweening. As you mentioned it will still cause a _parseTransform(). Well, it is really seems unnecessary to parse the previous transform as it will get completely cleared. Does GSAP need this to be able to clearProps the transform property?


If I replace the x property with opacity, that will cause a .getComputedStyle() to read the opacity's original value.

 

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>CodePen - GSAP Basic fromTo Tween</title>
    <style>
        .box {
            width: 200px;
            height: 200px;
            background: red;
        }
    </style>
</head>
<body>
<div class="box"></div>
<script src='https://s3-us-west-2.amazonaws.com/s.cdpn.io/16327/gsap-latest-beta.min.js'></script>
<script>
    setTimeout(function () {
        const el = document.querySelector('.box');

        el.style.left = '10px'; // Force recalculate style

        console.time("Set");
        gsap.set(el, {transform: 'none'});
        console.timeEnd("Set");


        setTimeout(function () {

            el.style.left = '20px'; // Force recalculate style

            console.time("FromTo");
            gsap.fromTo(el, {x: 0}, {duration: 5, x: 500});
            console.timeEnd("FromTo");
        }, 1000);
    }, 1000);
</script>

</body>
</html>

 

Does any of that make sense?

 

I could imagine a tween flag which would force GSAP to skip reading and store the from parameters into the cache or a new method to init the style cache with preset values on an element.

I see a flag something like that in the latest-beta which is not in the stable release: e.parseTransform, but it might not related: (g=t._gsap).renderTransform&&!e.parseTransform||We(t,e.parseTransform)

Link to comment
Share on other sites

1 hour ago, RolandSoos said:

Well, it is really seems unnecessary to parse the previous transform as it will get completely cleared. Does GSAP need this to be able to clearProps the transform property?

That isn't true actually. The transform property contains data for ALL of the following: x, y, z, rotation, rotationX, rotationY, scaleX, scaleY, skewX, skewY, plus it has to handle the transform-origin in a particular way when applying certain transforms to work around various browser bugs and inconsistencies. So if you merely set "x" on an element, that doesn't somehow clear all the other transform-related values. In fact, that's one of the huge benefits of using GSAP for transforms - it lets you handle all those components independently. One tween could be only touching "x", while another animates "y" using a totally different ease. If you tried doing that in CSS or most other animation libraries, those would overwrite each other and they wouldn't be synchronized/combined at all. 

 

Also, we can't assume that elements begin with an identity matrix (no transforms). Plenty of people set things up with CSS that has transforms, thus GSAP must honor those. That's why the initial parsing is essential. 

 

It has nothing to do with clearProps, FYI. 

 

The worst thing you can do performance-wise is directly set/animate the "transform" property because it requires extra parsing to accommodate literally any value which could be a super complex, strung-together list of many transforms. It's much faster to use the GSAP shortcuts directly, like x, y, scaleX, scaleY, rotation, etc. 

 

You must be dealing with a LOT of elements if you're able to notice any difference. Typically the parsing is very fast and nobody would notice any impact in a real-world project.  

  • Like 1
Link to comment
Share on other sites

Thank you for the detailed explanation!

 

52 minutes ago, GreenSock said:
2 hours ago, RolandSoos said:

Well, it is really seems unnecessary to parse the previous transform as it will get completely cleared. Does GSAP need this to be able to clearProps the transform property?

That isn't true actually. The transform property contains data for ALL of the following: x, y, z, rotation, rotationX, rotationY, scaleX, scaleY, skewX, skewY, plus it has to handle the transform-origin ...

I expressed myself badly, I know GSAP handles all of those. I wanted to tell that in my usage I know every property which is animated and they are set in the same tween, also I know that their pre-tween value is on specs default. This is why I could build a startValue cache with those values to prevent .getComputedStyle(). I know this is not the use case for most of the people and I do understand that it is enough for the most if the timeline is working as expected. 

 

Lighthouse has a total blocking time metrics which increases if a processing takes more than 50ms. Google measures this value on mobile (I use 4x cpu slow down for emulation) and building my timelines takes around 250ms. One .fromTo() takes between 8-20ms which is more than one frame in 60fps and I'm animating around 20 elements. It takes around 100ms for the .getTransform(). (Yes, I know it is not too much, I try to catch as many optimization as I can.)

 

Based on https://web.dev/, Google tries to points developers into the right direction to be able to create speedy websites. Key points are to minimalize recalculate style and layout. For example in the past I used .getBoundingClientRect().{width;height} to tell if an element is visible or not. This might result a layout which takes time. So my solution was to store the media queries on the HTML element when the element is not visible -> so I can  test the state with Window.matchMedia() without accessing .getBoundingClientRect() or .getComputedStyle() This was really just an example, but I think it shows how to prevent layout calculations.

 

Maybe this will be the future for GSAP too. We could prefill the element's transform/style cache, so GSAP wouldn't need to read those values. If there is no cache or the property does not exists, then usual GSAP behavior.  Non working example: 

See the Pen gOwzWJq by mm00 (@mm00) on CodePen

 

I do understand that this is not the most wanted feature :)

 

Link to comment
Share on other sites

Yeah, I understand what you mean and we can keep this kind of thing in mind for a potential future release if there's enough demand for it. We've gotta factor in kb cost too. It's always a tough balancing act between performance, API surface area, features, and file size. Thanks for the suggestion. 

  • Like 1
Link to comment
Share on other sites

Thank you Jack!

 

One interesting addition for this topic. Just added some measurement codes to GSAP:

_getComputedProperty = (target, property, skipPrefixFallback) => {
	console.time(property);
	let cs = getComputedStyle(target);
	const ret = cs[property] || cs.getPropertyValue(property.replace(_capsExp, "-$1").toLowerCase()) || cs.getPropertyValue(property) || (!skipPrefixFallback && _getComputedProperty(target, _checkPropPrefix(property) || property, 1)) || ""; //css variables may not need caps swapped out for dashes and lowercase.
	console.timeEnd(property);
	return ret;
},

 

It seems like getting transform origin results a Layout while getting transform is not and those transform origin reading calls sometimes took more than 10ms. Getting transform is cheap and took 0.2ms and getting the opacity was around 2ms.

 

image.thumb.png.8138f2eacacdf83e4f44933f2dc9dc30.png

 

  • Thanks 1
Link to comment
Share on other sites

  • 4 weeks later...

I was able to find the root cause of those layout processings. It seems like changing the opacity schedules a layout. So when I set the opacity on an element the next _parseTransform will recalculate style and layout instead of only recalculate style.

 

See the Pen vYyNOpV by mm00 (@mm00) on CodePen

 

image.thumb.png.6a62ff160a3005306c2a97af580d2253.png

Link to comment
Share on other sites

Thanks @RolandSoos. Did you have a particular recommendation related to this?

Link to comment
Share on other sites

@GreenSock I have a theory which creates two pipelines: one for property initialization/reading and one for rendering.

 

So if I have the following:

gsap.set("h1", {
  opacity: 0.5
});
gsap.set("p", {
  x: 100
});
gsap.fromTo("#a", 0.4, {
 opacity: 1,
 x: 0
}, {
 opacity: 0.5,
 x: 100
});

The current flow is:

  • h1: Read opacity [might have style recalculation]
  • h1: Render opacity
  • p: Read and prepare transform cache [style recalculation + layout <- caused by opacity change]
  • p: render transform
  • #a: Read opacity [style recalculation <- caused by transform change p]
  • #a: Render opacity
  • #a: Read and prepare transform [style recalculation + layout <- caused by opacity #a]
  • #a: render transform
  • Style recalculation [caused by transform change #a ] 

My idea:

  • Init Pipeline:
    • h1: Read opacity [might have style recalculation][optional <- set is exactly telling what opacity value]
    • p: Read and prepare transform cache
    • #a: Read opacity [optional <- from part is exactly telling what opacity value]
    • #a: Read and prepare transform cache
  • Render pipeline [Maybe in the next requestAnimationFrame]:
    • h1: Render opacity
    • p: render transform cache
    • #a: render opacity
    • #a: render transform
    • Style recalculation + layout

 

Link to comment
Share on other sites

A set() call must read the initial value because you could revert a set() call (so it has to store the initial value). Think of a set() that's in the middle of a timeline, for example - if you rewind and the playhead goes backward before that set() all, it must restore the initial value. A set() is just a zero-duration tween. So if I implement your suggestion, it'd break a lot of functionality and I'm sure we'd get a lot of complaints from people whose animations suddenly stop working correctly :)

 

Another reason it must do a read is because there can be relative values like x: "+=20" and some special properties require a read, like drawSVG where there's no such thing as just setting a CSS value to "20% 70%" - it must read the length of the path, etc. And anything transform-related must read the initial transforms so that it can properly segregate scaleX, scaleY, rotation, x, y, etc. and then build the proper string for the set. 

 

Unfortunately it's just not as simple as doing an element.style.property = value in a gsap.set(). But given those considerations, if you have other recommendations I'm all ears. 

  • Like 2
Link to comment
Share on other sites

I have another idea. A cache method in GSAP which could provide the required values what normally red from getComputedStyle.

gsap.cache("h1", {
  opacity: 1
});
gsap.set("h1", {
  opacity: 0.5
});
gsap.cache("p", {
  transform: none
});
gsap.set("p", {
  x: 100
});
gsap.cache("#a", {
  transform: none,
  opacity: 1
});
gsap.fromTo("#a", 0.4, {
 opacity: 1,
 x: 0
}, {
 opacity: 0.5,
 x: 100
});

The flow is (when all required values are provided in the cache method)

  • h1: Read opacity from [cache]
  • h1: Render opacity
  • p: Read and prepare transform from [cache]
  • p: render transform
  • #a: Read opacity from [cache]
  • #a: Render opacity
  • #a: Read and prepare transform from [cache]
  • #a: render transform
  • Style recalculation and layout
Link to comment
Share on other sites

The problem with that strategy is it would be twice as much work on every tick because it'd have to set the "real" value (element.style.opacity) AND the cached value (element._gsap.opacity) in order to stay properly synched. Totally not worth the tradeoff. Also, if we always read from the cache, it would ONLY work if the users set everything directly through GSAP. So if they used jQuery or set values manually or via CSS rules at some point, it'd all break. See what I mean? 

  • Like 3
Link to comment
Share on other sites

Yes, I totally understand these aspects. So I'm not sure what could be the solution :) 

 

I'm struggling to optimize the page load which contains several animated elements. On emulated 4x CPU slowdown the site takes 4 seconds until LCP, but 25-30% (1-1.5 sec) used for GSAP to create the timelines. I was only able to optimize one thing in my code:

adding display:none or not-attached dom elements to the timeline hugely slowing down the processing as GSAP display them or attach to the dom to be able to use getcomputed style, which cause multiple style recalculation and layout.

Link to comment
Share on other sites

Create an account or sign in to comment

You need to be a member in order to leave a comment

Create an account

Sign up for a new account in our community. It's easy!

Register a new account

Sign in

Already have an account? Sign in here.

Sign In Now
  • Recently Browsing   0 members

    • No registered users viewing this page.
×