Originally published on Substack (opens in a new tab)

Working with GSAP in a Nuxt3 project, can sometimes lead to performance issues (I know I did!). This happens due to tweens lingering in the JavaScript memory causing weird lags and slow downs that drag the user experience.

Plus, since Nuxt is a server-side rendered (SSR) framework by default, it becomes rather cumbersome with the cluttering of your components with repetitive boilerplate.

So the dev experience isn’t any fun either.

The Problem

So typically, it seems that GSAP animations are not automatically cleaned up, which can cause them to accumulate and bog down your application's performance.

Add in the fact Nuxt is SSR and therefore will execute JavaScript on the server before it hits the client-side, which doesn't have access to the DOM. We need to constantly use the onMounted hook.

Here is a common setup:

import { gsap } from "gsap";

onMounted(() => { 
//needs this hook since the script tag runs in SSR before the DOM is present.

   gsap.from(".site-header", {
      delay: "0.5",
      duration: 0.5,
      y: -50,
      opacity: 0,
      ease: "power4.out",
    });
  });

Introducing gsap.context()

I learnt about the gsap.context() method, which offers a neat solution by encapsulating all animations within a given context, making it easy to manage and dispose of them efficiently when they are no longer needed.

Here’s how you can better manage animations with gsap.context(), ensuring they don’t overstay their welcome,

Kill tweens after a time (Not Ideal):

import { gsap } from "gsap";

onMounted(() => { 
  let context = gsap.context(() => { //create context
    gsap.from(".site-header", {
      delay: "0.5",
      duration: 0.5,
      y: -50,
      opacity: 0,
      ease: "power4.out",
    });
  });

  setTimeout(() => {
    context.kill(); //kill the animation after a second
  }, 1000);
});

Kill when Component is unmounted:

import { gsap } from "gsap";

let context; //create this outside the hook to be able to use it in the second hook

onMounted(() => { 
  context = gsap.context(() => {
    gsap.from(".site-header", {
      delay: "0.5",
      duration: 0.5,
      y: -50,
      opacity: 0,
      ease: "power4.out",
    }); 
  });
});

onUnmounted(() => {
  context.kill();
});

Now that we've seen the headaches GSAP can cause in a Nuxt3 project. I’ll walk you through the best solution ive gathered to keep things smooth, simple and clean.

Lets dive!

Creating a GSAP Plugin for Nuxt

to streamline using GSAP in Nuxt3 we can create a plugin. This centralizes your GSAP logic, making it easier to maintain and reuse:

import { gsap } from "gsap";
import { ScrollTrigger } from "gsap/ScrollTrigger"; 

export default defineNuxtPlugin((nuxtApp) => {
  if (process.client) {
    gsap.registerPlugin(ScrollTrigger);
  }
  return { provide: { gsap: { gsap, ScrollTrigger } } }; 

//this allows you to use gsap and its plugins from a single object 'gsap' containing them at from useNuxtApp()
});
Make sure to add GSAP to the transpile array in your nuxt.config for optimal performance across all browsers:
export default defineNuxtConfig({
    build: {
        transpile: ["gsap"],
    },
...
}

The useGsap Composable

We can further encapsulate your GSAP logic within a custom composable for cleaner, more manageable code. This composable will wrap your animation contexts within its lifecycle hooks—specifically using onMounted for initialization and onUnmounted for cleanup.

Here’s how:

export default function useGsap(animationFunction) {
  // auto imported by nuxt
  const { $gsap } = useNuxtApp();

  // To ensure the animation function is called within the GSAP context
  let context; 
  // This context will automatically collect all animations created within it

  onMounted(() => {
    context = $gsap.gsap.context(() => {
      // Provide gsap and ScrollTrigger to the animation function
      if (animationFunction) animationFunction(...Object.values($gsap));
    });
  });

  // Automatically clean up when the component using this composable is unmounted
  onUnmounted(() => {
    // Kills all animations in the context, reverting their properties to the initial states
    context.kill();
  });

  return {
    ...$gsap, // allows you to destructure from the composble if needed
  };
}

Using the Composable

useGsap((gsap) => { //pass the plugins you need (gsap, scrollTrigger, ...)
  gsap.from(".site-header", {
    delay: "0.5",
    duration: 0.5,
    y: -50,
    opacity: 0,
    ease: "power4.out",
  }
});

Conclusion

Integrating GSAP into Nuxt3 efficiently can be challenging, but with the setup we've discussed, you can reduce complexity and enhance performance.

Encapsulating your animations in a custom composable, you ensure cleaner, more maintainable code and a smoother dev and user experience.

Check out this StackBlitz to see the composable in action and get a feel for how it can clean up your Nuxt3 projects.

Further reading:
Back to all writing