We spent six days on this GSAP resize bug

On June 16th, we released a new landing page for Rivian Quad after Pikes Peak race. This page took a lot of components that were built when we worked on Gen 2 landing pages which actually allowed us to ramp it up fairly quickly. In fact I think we actually finished 80% of the features in 4 days and spent the rest of sprint doing adjustments and optimizations. This was also the time we encountered a rather annoying and clueless bug with GSAP.

On the page, we have features and animations that depend on user’s scroll positions. Some elements will move or show a certain frame as the user scrolls the page. Sometimes the animation plays as scrolling happens and other time the animation would seem to be locked in place while scrolling. On the Quad page, we have around 7 scroll triggers like this and each of them needs to be pinned on the page for the animation to work.

typescript
ScrollTrigger.create({
  trigger: contentRef.current,
  pin: true,
  start: "top center",
  end: "+=500",
});

The other two properties that are essential to this functionality are the start and end. These tell GSAP when to trigger the animation sequence. It can be when the top of container is in the viewport or it reaches the center of the viewport. Now what’s the problem?

According to the documentation, ScrollTrigger should refresh when we resize the window or the viewport.

image

That makes sense since the content might change which means the start and end position and even the pin size might change too. Except in reality when we resize the page, these animations either interfere or overlap with each other.

Refresh Priority

We started reading the documentation to see if there was anything related to resizing or refreshing the triggers. In ScrollTrigger, there was a refreshPriority that we can set in each ScrollTrigger. The idea is giving it a number and whenever ScrollTrigger needs to refresh it will sort them and refresh them in an order. Although it didn't fix our problem but we thought it was important so we kept them to have more definitive control over how ScrollTrigger refreshes.

Manual Refresh

I couldn’t find the original thread now but there was a similar discussion somewhere and eventually it was suggested that there might be a synchronization timing between when ScrollTrigger would call the refresh method and when the content would finish updating. While we weren’t sure if it was our case, we manually called ScrollTrigger.refresh() in each animation component after 2,500 ms. That actually did the trick! This eventually evolves into utilizing resize event and debounce. It looks something like this. I think this ScrollTrigger actually holds reference to all the other triggers created by ScrollTrigger.create() so this snippet could exist in the parent component and we only needed to do it once.

typescript
useEffect(() => {
  const onResize = debounce(() => {
    ScrollTrigger.refresh();
  }, 1500);

  window.addEventListener('resize', onResize);
  onResize();

  return () => {
    window.removeEventListener('resize', onResize);
  };
}, []);

Bonus: useGSAP

When we were researching how to use GSAP with React, we found out they actually provided a useGSAP hook which should make animations and cleanup easier for us developers. During debugging, we found out these triggers started multiplying when we were resizing the browser. We did have some components that were specific to different viewport but if cleanup was done correctly those triggers shouldn't multiply or linger around when they unmounted. Since we didn't really understand what useGSAP actually did, we replaced all of them with useEffect and return the cleanup function ourselves. Guess what, that did it. 🙈

typescript
useEffect(() => {
  const animationScroll = ScrollTrigger.create({
    trigger: containerRef.current,
    start: () => "top top",
    end: () => "bottom top",
    pin: true,
    invalidateOnRefresh: true,
  });

  return () => {
    animationScroll.kill();
  };
}, []);

I have to say, I think this was very satisfying when we solved this problem. We half-jokingly mentioned we could refresh the page when users resize the viewport and we were happy we didn’t actually come to that.

If you find this useful please follow and share it on X or Threads.