Jump to content
Search Community

ScrollTrigger.batch bug with "once" ScrollTrigger's

Cuberto test
Moderator Tag

Go to solution Solved by GreenSock,

Recommended Posts

Hello!

 

First of all, I want to say thank you to GSAP team for this wonderful tool that we have been using for many years ❤️

 

Recently I've found a bug related to ScrollTrigger.batch and ScrollTrigger's with "once: true" option. It can be simply reproduced by opening the page below, scrolling to the middle or end of the page, and then clicking Refresh in your browser. 

 

Looking at the console you can see: 

Uncaught TypeError: Cannot read properties of undefined (reading 'end')
    at Te.refresh (ScrollTrigger.min.js:10:25526)
    at ScrollTrigger.init (ScrollTrigger.min.js:10:32732)
    at new ScrollTrigger (ScrollTrigger.min.js:10:36936)
    at ne.create (ScrollTrigger.min.js:10:37248)
    at ScrollTrigger.min.js:10:38361
    at Array.forEach (<anonymous>)
    at ne.batch (ScrollTrigger.min.js:10:38289)
    at HTMLDocument.<anonymous> (minimal.html:142:20)

 

I can’t show it on CodePen because the scroll is reset in the iframe after the page refreshed. 

I'm not sure when this appeared, but on the latest GSAP versions 3.12.4 and 3.12.5 the bug is present.

 

Minimal demо: https://j4detdwi8u.demo.cubdev.com/

Super minimal demо: https://j4detdwi8u.demo.cubdev.com/minimal.html

Possible workaround:  https://j4detdwi8u.demo.cubdev.com/workaround.html

 

Env:

Chrome  122.0.6261.129 Win64

Firefox 122.0 Win64

Chrome 122.0.6261.129 Mac ARM64

This cannot be reproduced in Safari 17.0 Mac ARM64

 

Kind Regards,

 

Artem, Cuberto

Link to comment
Share on other sites

  • Solution

Great catch, @Cuberto! Sorry about any confusion there. This would only happen if the very first ScrollTrigger (in terms of refreshPriority/order) has once: true. It should be resolved in the next release which you can preview at: 

https://assets.codepen.io/16327/ScrollTrigger.min.js (you may need to clear your cache)

 

Better? 

  • Like 2
Link to comment
Share on other sites

By the way, there's a very easy workaround if you don't want to wait for the patch to be released - just create a simple ScrollTrigger that doesn't do anything first: 

See the Pen eYogRgB?editors=0010 by GreenSock (@GreenSock) on CodePen

 

(uncomment line 4 to see it work)

 

Thanks for putting together such a great minimal demo. That makes troubleshooting much easier. 🙌

Link to comment
Share on other sites

@GreenSock Thank you very much for your help!


I would like to take this opportunity to ask you. A little off topic, I hope you don’t mind.

 

For some animations with CSS transformations, we explicitly set will-change: transform to create a composite layer that will be processed on the GPU and eliminate jitter, especially on text. Of course, after performing the transformations, will-change must be returned to the auto in order to free up memory.

 

I do it like this:

const tl = new gsap.timeline();

tl.set(elements, {
    willChange: 'transform',
}, 0);

tl.fromTo(elements, {
    y: '110%',
    scaleY: 1.5,
    transformOrigin: 'top top',
}, {
    y: '0%',
    scaleY: 1,
    transformOrigin: 'top top',
    duration: 2,
    stagger: {amount: 0.3},
    ease: 'expo.out',
}, 0);

tl.set(elements, {
    willChange: 'auto',
});

return tl;

 

Some developers I know do it like this:

const tl = new gsap.timeline();
const wc = [...elements, ...moreElements];

tl.set(wc, {
    willChange: 'transform',
}, 0);

tl.fromTo(elements, {
    y: '110%',
    scaleY: 1.5,
    transformOrigin: 'top top',
}, {
    y: '0%',
    scaleY: 1,
    transformOrigin: 'top top',
    duration: 2,
    stagger: {amount: 0.1},
    ease: 'expo.out',
}, 0);

tl.fromTo(moreElements, {
    y: '110%',
}, {
    y: '0%',
    duration: 2,
    stagger: {amount: 0.3},
    ease: 'expo.out',
}, 0.2);

tl.set(wc, {
    willChange: 'auto',
});

return tl;

 

Unfortunately, I did not find a built-in mechanism in GSAP to automate this routine process. There is a Force3D property, but it’s not quite the same. It would be very cool if GSAP could do this automatically, for example:

tl.from(elements, {
    y: '110%',
    scaleY: 1.5,
    duration: 2,
    stagger: {amount: 0.3},
    ease: 'expo.out',
    autoWillChange: true, // or force3D: 'will-change'
});

 

Here are some videos where you can see the difference when using will-change.

The difference is clearly visible at the “We would love to talk!” caption.

 

No will-change: https://www.dropbox.com/scl/fi/sau2hmz055zuylq67pqsq/no-will-change.mp4?rlkey=4buyngxk359nxioxyc3l4i07a&dl=0

Will-change is set: https://www.dropbox.com/scl/fi/8fkxxh6j6w79fn9bcjdy0/will-change-set.mp4?rlkey=ycv5aq4qir6vpzvznn8frdur7&dl=0

Force3D is true: https://www.dropbox.com/scl/fi/ww2ienyi4kl6067xz4t5u/force3D-true.mp4?rlkey=ssx678lh215pfyk73t3nc8xgj&dl=0

 

Once again, sorry for being off-topic, I probably should have posted this as a future request.

Link to comment
Share on other sites

Yeah, it's a little frustrating that the browsers moved the goal post and switched up how things work (they've gone back and forth actually). will-changed is really a mixed bag. Sometimes it helps, sometimes it hurts. And sometimes it depends on which browser. 

 

Here's the real question: have you ever seen any real-world benefit to switching back and forth between will-change values? Why not just set will-change: transform in the CSS and leave it alone? I know you mentioned memory savings and in theory that's true, but you also need to factor in the cost of switching that value back and forth because that takes CPU cycles and involves funneling data to the GPU. In other words, you actually may see a DECREASE in overall performance by toggling like that. So if I were you, I'd do some tests in your particular scenario to see if there's really any benefit one way or the other. 

 

I'll keep this in mind as a possible enhancement for a future release (autoWillChange), but it'd also be pretty easy to write a helper function that does it for you: 

 

function autoWillChange(vars) {
  let setTo = (callback, value) => {
    let orig = vars[callback];
    vars[callback] = function() {gsap.set(this.targets(), {willChange: value}) && orig && orig.call(this); }
  };
  setTo("onStart", "transform");
  setTo("onComplete", "auto");
  return vars;
}

Usage

gsap.to(".container", autoWillChange({
	x: 100,
	duration: 2,
	onComplete() {
		console.log("complete");
	}
}));

Just wrap it around your vars object and you're good-to-go. You don't need to use those extra .set() calls at the beginning and ending of those timelines. 

 

But again, I personally would probably just set will-change: transform in the CSS and leave it alone because in most cases the memory won't be an issue and it'll deliver better performance not to shift the value around.  

 

I hope that helps!

  • Like 2
Link to comment
Share on other sites

And here's another way to do it - a custom plugin: 

gsap.registerPlugin({
	name: "autoWillChange",
	init(target, vars, tween) {
		this.target = target;
		this.active = false;
		this.tween = tween;
		return !!vars;
	},
	render(ratio, data) {
		let progress = data.tween.progress(),
			active = (progress > 0 && progress < 1);
		if (active !== data.active) {
			data.active = active;
			data.target.style.willChange = active ? "transform" : "auto";
		}
	}
});

Usage: 

gsap.to(".container", {
  x: 100,
  duration: 2,
  autoWillChange: true, // <-- magic!
  onComplete() {
    console.log("complete");
  }
});

🥳

  • Like 3
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.
×
×
  • Create New...