gabriel.ortiz Posted May 1, 2024 Posted May 1, 2024 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!
Rodrigo Posted May 1, 2024 Posted May 1, 2024 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: Try setting will-change: transform on the CSS of your moving elements. Make sure you're animating transforms (like x, y) instead of layout-affecting properties like top/left. Definitely avoid using CSS filters or things like blend modes. Those are crazy expensive for browsers to render. 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. 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) 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. 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. 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. 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!
gabriel.ortiz Posted May 1, 2024 Author Posted May 1, 2024 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";
Rodrigo Posted May 1, 2024 Posted May 1, 2024 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 ?♂️
gabriel.ortiz Posted May 1, 2024 Author Posted May 1, 2024 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; }
Solution Rodrigo Posted May 1, 2024 Solution Posted May 1, 2024 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!
Recommended Posts
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 accountSign in
Already have an account? Sign in here.
Sign In Now