Jump to content
Search Community

Nested timeline renders on next frame

wieslawski test
Moderator Tag

Go to solution Solved by GreenSock,

Recommended Posts

I am using GSAP to create multiple complex animations. Parts of such animations are detached from the main timeline - at certain points new timelines are created using a callback function. The callback function is required, because nested timelines are created dynamically and can be repeated infinitely.

The problem is that nested timelines start running a few milliseconds later (I assume that the reason is that a created timeline is processed at the next tick). This few milliseconds can result in rendering nested timeline with a single frame delay.

The fix I found is to nest timeline like this (in codepen demo I did not include the suppressEventsparameter so that nested timeline can print output): 

.add(() => createNestedTimeline().progress(1, true).progress(0, true))

It doesn't look pretty though. Is there a way to do this properly?

See the Pen MWZOxWJ by Dariusz-Zarzycki (@Dariusz-Zarzycki) on CodePen

Link to comment
Share on other sites

Hi @wieslawski welcome to the forum!


instead of setTimeout can you try using gsap.delayedCall() https://greensock.com/docs/v3/GSAP/gsap.delayedCall(), this will be much more in sync with the rest of your GSAP code. 


Can you maybe also try to explain the base issue you're solving, compared to the problem you're facing now. Maybe there is a way more easy solution that you did not think about yet. Read https://xyproblem.info

  • Like 2
Link to comment
Share on other sites

Hey @mvaneijgen!


Thank you for your reply. I modified codepen demo according to your advice.


The base issue is:

I want to create an animation (let's call it OUTER_ANIM). At certain point in time of that animation I would like to start an independent, dynamically created animation (let's call it NESTED_ANIM). At the same point I would like to continue OUTER_ANIM. I would like the effects of continuation of OUTER_ANIM and start of NESTED_ANIM to be rendered in the same frame.


My approach is to create a timeline, and add a callback that creates the nested timeline. The result is that continuation of the outer timeline starts a few milliseconds before the start of the nested timeline (V1 in codepen demo) which can result in a single frame difference.

If I modify the approach by adding .progress(1).progress(0) (V2 in codepen demo) than it works fine, but I guess that this is not a proper way to achieve the goal.


Link to comment
Share on other sites

I would just add that animation to a timeline and have it start at the position you want with the position parameter. Here I have a timeline that moves a box and then at 1 second it will add a completely new timeline to the current timeline which has an animation that rotates the same box infinitely. 


See the Pen yLGPWLd?editors=0010 by mvaneijgen (@mvaneijgen) on CodePen

Link to comment
Share on other sites

@mvaneijgen Thanks for your code. Unfortunately it does not meet my needs. Main timeline does not end (as I stated, the nested animation should be independent - meaning it should not affect the outer timeline) and there is no continuation to it.

However if I modify it just a little bit it becomes a great example of what I mean:

See the Pen WNLXBEB by Dariusz-Zarzycki (@Dariusz-Zarzycki) on CodePen


Each time you run it you can get a different result, but quite often you'll see result like:

MAIN TIMELINE CONTINUE, time passed: 996
ROTATING BOX STARTED, time passed: 1002

And this may cause 1 frame delay (between nested animation start, and main animation continuation) which is unaccaptable for me.

Link to comment
Share on other sites



The amount of milliseconds between the start of the animation triggered by the callback is most likely tied to the time needed to create the log or execute the onStart callback. In your last post you  have a difference of 6 milliseconds, which is basically not detectable by the human eye at all, but is the amount of time it takes to actually execute that particular code, nothing more.


To be clear, you are creating and returning a GSAP Timeline instance using the add() method. That basically adds that instance to the timeline and the next animation will be added after that particular instance. But in this case you are also using a label so all the instances added at the same point and start at the same time. You can check this with the following code:

const child = tl.getChildren();
child.forEach((e, i) => {
  console.log(i, e.startTime());

Honestly I don't see how this is a GSAP issue, IMHO is just the way things work when it comes to JS execution context and call stack works:




Hopefully this clear things up.

Happy Tweening!

Link to comment
Share on other sites

I think you might be misunderstanding when onStart fires. According to the docs: 


A function that should be called when the animation begins (when its time changes from 0 to some other value which can happen more than once if the tween is restarted multiple times).


You seem to be expecting it to fire the exact moment you CREATE the animation. But at that moment, the playhead is at 0. On the very next tick, the playhead updates and moves past 0 and THAT is when onStart fires. It's when the playhead shifted away from 0. 


From what I can tell, it's doing exactly what it's supposed to do in all your demos. 


So let's walk through this: you're placing a callback at exactly 1 second into the main timeline. When the playhead either lands directly on that spot or anywhere PAST that spot (let's say it's 1.02), it'll fire that callback. Now inside that callback, you're creating a new timeline with an onStart. But at this point, its playhead hasn't moved at all. According to the original timeline's time, this new one starts at 1.02. On the very next tick, it updates (let's say 0.016 seconds later), and that's when the onStart of that new timeline would fire. 


The other small problem is that you're using Date.now() for the startTime which means you're not synchronizing time with the GSAP core ticker. The entire GSAP ecosystem is completely synchronized, all based on that ticker. When you load web pages, you have no idea how long that's going to take. Your Date.now() might be at the start or at the middle or at the end of that whole startup routine that could take dozens or even hundreds of milliseconds. It's not a safe way to synchronize things. 


If I understand your goal correctly, you'd need to adjust your new timeline's playhead according to the playhead of that parent (the one with the callback). Figure out how far past the embedded spot the playhead is. For example, if the playhead goes 0.01 seconds past that spot, you should set that new timeline's playhead to 0.01 right after creating it. 


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


Does that clear things up?

  • Like 1
Link to comment
Share on other sites


In the first post I wrote:

11 hours ago, wieslawski said:

The problem is that nested timelines start running a few milliseconds later (I assume that the reason is that a created timeline is processed at the next tick)

So I was expecting this to be the problem. Regardless, thank you so much for your elaborated response, I was not aware of registering the time difference. This makes your solution is better than mine (V2 from the first post), because nested timeline is in sync with outer one.


I have one last question - is there no easy way to add a synced timeline so it doesn't affect the parent's total time? 

If not, what do you think of this solution (adding method to Timeline's prototype, that uses your approach):

See the Pen RwExRQX by Dariusz-Zarzycki (@Dariusz-Zarzycki) on CodePen


Obviously, it can be easily modified to use custom positions as well. 


Link to comment
Share on other sites

  • Solution

I'm not a fan of that approach because:

  1. Adding to the GSAP Timeline prototype gives your code a funky "smell" because anyone else looking at it might think "wait, I didn't know GSAP had a method like that..." (and it doesn't). 
  2. You're relying on Math.random() always being perfectly unique but that isn't bulletproof. You might run into a bug like that where one gets overwritten. Unlikely, but possible. There's really no reason to use a label anyway - you can just use this.duration() as the insertion point.
  3. Your code assumes the function will always return an animation which may be true in your particular case, but if I were you I'd make it more bulletproof. 

Here's another approach that [in my opinion] is cleaner: 

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


You just wrap synced(...) around your function and that's it. Notice that'll also work fine if you add it at a certain position, like: 

.add(synced(createNestedTimeline), 2)


I hope that helps!

  • Like 2
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...