Jump to content
Search Community

Animating TranslateY noticing flickering in React

gabriel.ortiz test
Moderator Tag

Go to solution Solved by Rodrigo,

Recommended Posts

Hi yall, I'm hoping I can get some help with some gsap animations in React. I have a UI i'm working on where an inactive segment animates by translating up, while a new active element translates to 0 from the bottom.. There is a noticeable flicker of no content before the animation starts. I don't know what I'm doing wrong to cause this. I first noticed it in my storybook.

 

Here's a link to my code transferred to a codesandbox to demonstrate; this is the core of the logic where the animation happens: https://codesandbox.io/p/sandbox/gsap-expedition-w96d45?file=%2Fsrc%2Fcomponents%2FExpedition%2FSegment.tsx%3A189%2C28 

 

You can recreate the animation sequence by clicking on the buttons at the top which should trigger the animations to change. Nothing will happen if you click the same button twice

 

 

 

Caveat: I know I should be using useGsap hook - but I can't until we update our react version. So here i use a custom hook instead.


Any help would really be appreciated!image.thumb.png.3cc5cc5c54c4be0d2391ac797753bbfa.png

Link to comment
Share on other sites

  • gabriel.ortiz changed the title to Animating TranslateY noticing flickering in React

Hi,

 

Your sandbox is not working, the response is a 404 Sandbox not found.

 

Maybe you're having issues with FOUC, check this article from our Learning Center:

https://gsap.com/fouc

 

Finally a lot of performance problems are down to how browsers and graphics rendering work. It's very difficult to troubleshoot blind and performance is a DEEP topic, but here are some tips: 

  1. Try setting will-change: transform on the CSS of your moving elements. 
  2. Make sure you're animating transforms (like x, y) instead of layout-affecting properties like top/left. 
  3. Definitely avoid using CSS filters or things like blend modes. Those are crazy expensive for browsers to render.
  4. Be very careful about using loading="lazy" on images because it forces the browser to load, process, rasterize and render images WHILE you're scrolling which is not good for performance. 
  5. Make sure you're not doing things on scroll that'd actually change/animate the size of the page itself (like animating the height property of an element in the document flow)
  6. Minimize the area of change. Imagine drawing a rectangle around the total area that pixels change on each tick - the bigger that rectangle, the harder it is on the browser to render. Again, this has nothing to do with GSAP - it's purely about graphics rendering in the browser. So be strategic about how you build your animations and try to keep the areas of change as small as you can.
  7. If you're animating individual parts of SVG graphics, that can be expensive for the browser to render. SVGs have to fabricate every pixel dynamically using math. If it's a static SVG that you're just moving around (the whole thing), that's fine - the browser can rasterize it and just shove those pixels around...but if the guts of an SVG is changing, that's a very different story. 
  8. data-lag is a rather expensive effect, FYI. Of course we optimize it as much as possible but the very nature of it is highly dynamic and requires a certain amount of processing to handle correctly.
  9. I'd recommend strategically disabling certain effects/animations and then reload it on your laptop and just see what difference it makes (if any).

Hopefully this helps.

Happy Tweening!

Link to comment
Share on other sites

OMG @Rodrigo i'm sorry i realized my codesandbox was private: Here you can try again: https://codesandbox.io/p/sandbox/gsap-expedition-w96d45?file=%2Fsrc%2Fcomponents%2FExpedition%2FSegment.tsx Thank you for your time if you have any. I appreciate it

Also, this is the core animation segment for reference:

import React, { forwardRef, useEffect, useRef } from "react";
import styles from "./styles.module.scss";
import { useActiveSegment, BkExpeditionContextValues } from ".";
import { mergeRefs } from "../util/mergeRefs";
import clsx from "clsx";
import { useGsapContext } from "../util/useGsapContext";
import gsap from "gsap";

/**
 * Registered animations for the context
 */
export type RegisteredContextAnimations = {
  /**
   * Animation to play when the segment is active
   */
  segmentIn: () => gsap.core.Timeline;
  /**
   * Animation to play when the segment is inactive
   */
  segmentOut: () => gsap.core.Timeline;
};

/**
 * Segment component to be used within the BkExpedition component
 */
export type SegmentProps = React.ComponentPropsWithoutRef<"section"> & {
  segmentKey: BkExpeditionContextValues["activeSegment"];
};

export const Segment = forwardRef<HTMLDivElement, SegmentProps>(
  (props, ref) => {
    const {
      segmentKey,
      "aria-label": ariaLabel,
      className,
      children,
      ...restOfHtmlAttrs
    } = props;

    /**
     * Get the active segment from the context
     */
    const { activeSegment, previousSegment } = useActiveSegment();

    /**
     * Check if the segment is active
     */
    const isActive = activeSegment === segmentKey;

    /**
     * Reference to the segment
     */
    const sectionRef = useRef<HTMLDivElement>(null);

    /**
     * Get the context for the animations
     */
    const ctx = useGsapContext<HTMLDivElement, RegisteredContextAnimations>(
      sectionRef
    );

    /**
     * Animation constants
     */
    const ANIMATION_DURATION = 1;
    const ANIMATION_SCALE = 0.9;
    const ANIMATION_EASING = "power1.inOut";

    const TRANSLATION_BASE = {
      y: 0,
      x: 0,
    };

    useEffect(() => {
      /**
       * Register the animations for the segment
       */
      ctx.add("segmentIn", () => {
        return gsap
          .timeline({
            paused: true,
            onComplete: () => {
              sectionRef.current?.focus();
            },
          })
          .fromTo(
            sectionRef.current,
            {
              ...TRANSLATION_BASE,
              yPercent: 100,
              autoAlpha: 0,
            },
            {
              ...TRANSLATION_BASE,
              yPercent: 0,
              autoAlpha: 1,
              scale: 1,
              duration: ANIMATION_DURATION,
              ease: ANIMATION_EASING,
              immediateRender: false,
            }
          );
      });

      /**
       * Register the animations for the segment to animate out
       */
      ctx.add("segmentOut", () => {
        return gsap
          .timeline({
            paused: true,
            onComplete: () => {
              gsap.set(sectionRef.current, {
                ...TRANSLATION_BASE,
                yPercent: 100,
                autoAlpha: 0,
                scale: 0.95,
              });
            },
          })
          .fromTo(
            sectionRef.current,
            {
              ...TRANSLATION_BASE,
              yPercent: 0,
              autoAlpha: 1,
              scale: 1,
            },
            {
              ...TRANSLATION_BASE,
              yPercent: -100,
              autoAlpha: 0,
              scale: ANIMATION_SCALE,
              duration: ANIMATION_DURATION,
              ease: ANIMATION_EASING,
              immediateRender: false,
            }
          );
      });

      return () => {
        ctx.revert();
      };
    }, []);

    useEffect(() => {
      if (previousSegment === null) {
        return;
      }

      if (activeSegment === segmentKey) {
        if (ctx && "segmentIn" in ctx) {
          ctx?.segmentIn?.().play();
        }
        return;
      }

      if (previousSegment === segmentKey) {
        if (ctx && "segmentOut" in ctx) {
          ctx?.segmentOut?.().play();
        }

        return;
      }

      return () => {
        ctx.revert();
      };
      // only run when the active segment changes
      // eslint-disable-next-line react-hooks/exhaustive-deps
    }, [activeSegment, previousSegment, segmentKey]);

    /**
     * Add error handling for when the segmentKey is not set
     */
    if (!segmentKey || segmentKey === "") {
      if (process.env.NODE_ENV === "development") {
        throw new Error(
          "The segmentKey prop must be passed to the BkExpedition.Segment component"
        );
      }

      return null;
    }

    const inlineStyles: React.CSSProperties = {
      ...(previousSegment === null && {
        transform: isActive ? "translateY(0%)" : "translateY(100%)",
        opacity: isActive ? 1 : 0,
      }),
    };

    return (
      <section
        ref={mergeRefs(ref, sectionRef)}
        {...restOfHtmlAttrs}
        aria-hidden={!isActive}
        tabIndex={-1}
        aria-label={ariaLabel || segmentKey}
        className={clsx(
          className,
          styles.segment,
          isActive ? styles.isActive : styles.isInactive,
          isActive && previousSegment === null && styles.isInitial
        )}
        style={{}}
        data-segment-key={segmentKey}
        data-segment-active={isActive}
        data-testid={`${segmentKey}-segment`}
      >
        {children}
      </section>
    );
  }
);

Segment.displayName = "Segment";

 

Link to comment
Share on other sites

Hi,

 

The animation looks good to me, I can't see any flickering/jittering or any other issue while changing the active element, everything looks fluent and nice on my end  on Ubuntu 22 & 20 on the latest Firefox and Chrome 🤷‍♂️

Link to comment
Share on other sites

Maybe it was hardware acceleration issues? This morning, I also changed something in my storybook that seemed to help - even though I can't explain why. 

 

On initial load, I set the initial segment position with GSAP, and that seemed to make a huge difference. Any theories why setting GSAP styles on the initial item made the difference? The flickering i was seeing seemed to happen for the first transition between segments.

 

After this change, it seems to be fluid. But I don't understand why. Is it best practice to set initial GSAP styles on all elements before animating? Thanks so much for your help @Rodrigo
 

This is what i added:

  /**
   * Animation to play when the segment is the initial segment
   */
  setInitialSegment: () => gsap.core.Timeline;

....

// For setting the initial animation state with gsap values
      ctx.add("setInitialSegment", () => {
        return gsap.set(sectionRef.current, {
          ...TRANSLATION_BASE,
          yPercent: 0,
          autoAlpha: 1,
          scale: 1,
        });
      });

....

    useEffect(() => {
      /**
       * If the previous segment is null, then this is the initial segment
       */
      if (previousSegment === null) {
        /**
         * Establish gsap styles on the initial segment
         */
        if (isActive) {
          if (ctx && "setInitialSegment" in ctx) {
            ctx?.setInitialSegment?.();
          }
        }

        return;
      }

 

Link to comment
Share on other sites

  • Solution

That is mostly because GSAP handles all the transforms using the 3D transform matrix:

https://developer.mozilla.org/en-US/docs/Web/CSS/transform-function

https://developer.mozilla.org/en-US/docs/Web/CSS/transform-function/matrix3d

 

While setting a single transform using CSS does just that, applies a single transform. When you do that and then animate the same property with GSAP, GSAP takes that value applied by CSS and uses it's own method for transforms. That is why we recommend doing what you tried and worked, use GSAP to  set all the initial transforms that will be animated with GSAP, to avoid that extra step when the animation first runs. Also sometimes is a good idea to use the advice from the FOUC article to prevent annoying flashes before the JS runs.

 

Happy Tweening!

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...