What’s the Issue?

I was recently working on a client’s landing page that included a large animation on page load. This was implemented as a CSS animation that would play immediately once the page had rendered. The problem was this animation began very jittery as the frame rate of the page dropped significantly while the rest of the page continued loading. The remainder of the page was doing a number of needed async requests and loading a sizable amount of media at the same time, including a couple looping video files, consuming a decent amount of computer resources while attempting to play the CSS animation. Obviously, not an ideal first impression on page load.

My First Attempt

In this case, the CSS animation had no delay and was applied directly to the base styles of the element, so my first idea was to move the styles for the animation to their own class then dynamically add this new animation class to the element with useEffect. By passing an empty array of dependencies to useEffect, the function would only run once when the component had mounted and rendered - ensuring the component itself was ready to animate. I used a simple state, set to false by default, to dynamically add the class to the element.

SCSS

.myAnimation {
  animation-name: myAnimation;
  animation-duration: 5s;
}

JavaScript

import { useEffect, useState } from 'react';
import classNames from 'classnames';

const MyComponent = () => {
  const [playAnimation, setPlayAnimation] = useState(false);

  // This will run one time after the component mounts
  useEffect(() => {
    setPlayAnimation(true);
  }, []);

  return (
    <div
      className={classNames('myComponentClass', { myAnimation: playAnimation })}
    />
  );
};

export default MyComponent;

This change delayed the start of the animation by around ~75 milliseconds in my case, and while this slightly improved the issue, some of the animation was still overlapping with the resource intensive page load. So while better, the beginning of animation was still plagued by a low frame rate.

The Solution

To delay the animation further, and ensure no overlap with the rest of the page loading, I decided to fully wait for the window load event before starting the animation. This would wait for all dependent resources (such as stylesheets and images) to be loaded before my animation started. While this would delay the animation further, it would happen while the user was waiting for the page to load anyway and would guarantee a smooth animation.

To accomplish this I beefed up the useEffect with a window.addEventListener and a simple onPageLoad function to set the animation state. Because useEffect is called after component render, we need to double-check the window load event has not already fired. To do this I added an if statement before the event listener, checking the readyState of the document. I also added a return function for the useEffect, cleaning up the even listener if the component was ever unmounted.

SCSS

.myAnimation {
  animation-name: myAnimation;
  animation-duration: 5s;
}

JavaScript

import { useEffect, useState } from 'react';
import classNames from 'classnames';

const MyComponent = () => {
  const [playAnimation, setPlayAnimation] = useState(false);

  // This will run one time after the component mounts
  useEffect(() => {
    const onPageLoad = () => {
      setPlayAnimation(true);
    };

    // Check if the page has already loaded
    if (document.readyState === 'complete') {
      onPageLoad();
    } else {
      window.addEventListener('load', onPageLoad);
      // Remove the event listener when component unmounts
      return () => window.removeEventListener('load', onPageLoad);
    }
  }, []);

  return (
    <div
      className={classNames('myComponentClass', { myAnimation: playAnimation })}
    />
  );
};

export default MyComponent;

And #BOOM - works like a charm! The onPageLoad function waits for the full page load, and then the animation starts smooth as butter. The important thing to remember with this solution is to clean up the event listener and check the document readyState; without that check, it is possible our animation would never run if the component was mounted after the window load event had already fired. Hopefully, my work here helps you out! And, as always, feel free to reach out if you have any comments or questions. Thanks for reading!