mifi/react-lottie-player

Lazy loading animation data would be very handy

EliotSlevin opened this issue · 8 comments

This might be far beyond the scope of this library - but being able to load animation data when it enters the viewport would be great.

Including animation data via import is good, but if you've got quite a few complex animations on a page, this can increase your bundle size real fast. Particularly if inline bitmaps are part of an animation. I'm working on a project where our average lottie json is ~1mb, and we've got like 6 per page. That's a chunky bundle! A chunky bundle that's getting in the way of my performance scores!

A lazyload feature, built into a library like this could be hella handy. Something like

<Lottie
    loop
    play
    animationData={'./some/static/path.json'}
    lazyLoad
    placeholder={placeholder.svg}
/>

The Interaction Observer API would be perfect to check if the lottie is in viewport - then if it is, fetch the json and fire up the animation.

Would be very nice.

mifi commented

Hi. I think you can solve the bundle size with code splitting:

const MyComponent = () => {
  const [animationData, setAnimationData] = useState();

  useEffect(() => {
    import('./animation.json').then(setAnimationData);
  }, []);

  if (!animationData) return <div>Loading...</div>;
  return <Lottie animationData={animationData} />;
}

See https://reactjs.org/docs/code-splitting.html

Then you can mount this component only when it enters the view. Can be achieved with something like this:
https://github.com/olistic/react-use-visibility

Just if anybody ends up finding this in the future, I solved this problem just as you suggested, with a component like this.

import React, { useEffect, useState } from 'react'
import Lottie from 'react-lottie-player'

const Animation = ({animationName, placeholder}) => {
    const [animationData, setAnimationData] = useState()
  
    useEffect(() => {
      import(`./lotties/${animationName}`).then(setAnimationData)
    }, [])
  
    if (!animationData) return <img src={placeholder}/>
    return <Lottie animationData={animationData} play loop/>
  }
export default Animation

When the component first mounts, it renders the SVG placeholder I've passed through - all in nice small bundle. At this point the user can start interacting with the page, then it starts loading the animation data, then renders it once it's all available.

I've found that just doing that is enough to dramatically improve the performance score. Detecting what Animations were in the viewport and then loading just those files just wasn't needed for my project.

Thanks for the help and banging library!

Just if anybody ends up finding this in the future, I solved this problem just as you suggested, with a component like this.

import React, { useEffect, useState } from 'react'
import Lottie from 'react-lottie-player'

const Animation = ({animationName, placeholder}) => {
    const [animationData, setAnimationData] = useState()
  
    useEffect(() => {
      import(`./lotties/${animationName}`).then(setAnimationData)
    }, [])
  
    if (!animationData) return <img src={placeholder}/>
    return <Lottie animationData={animationData} play loop/>
  }
export default Animation

When the component first mounts, it renders the SVG placeholder I've passed through - all in nice small bundle. At this point the user can start interacting with the page, then it starts loading the animation data, then renders it once it's all available.

I've found that just doing that is enough to dramatically improve the performance score. Detecting what Animations were in the viewport and then loading just those files just wasn't needed for my project.

Thanks for the help and banging library!

@EliotSlevin Ever encountered this error? Whenever I use your code nothing loads. JSON files work just fine without lazy-loading and I have tried multiple JSONs from LottieFiles

import React, { useEffect, useState } from 'react';
import Lottie from 'react-lottie-player';

const Hero = () => {
  const [animationData, setAnimationData] = useState();

  useEffect(() => {
    import(`../../lottie/woman.json`).then(setAnimationData);
  }, []);

  if (!animationData) return <div>Loading...</div>;
  return <Lottie animationData={animationData} play loop />;
};
export default Hero;

Lottie File

Keep getting SVG errors in console:
image

Edit: Resolved it by using the below

import React, { useEffect, useState } from 'react';
import Lottie from 'react-lottie-player';

const Hero = () => {
  const [animationData, setAnimationData] = useState();

  useEffect(() => {
    import(`../../lottie/woman.json`).then((res) => setAnimationData(res.default));
  }, []);

  if (!animationData) return <div>Loading...</div>;
  return <Lottie animationData={animationData} play loop />;
};
export default Hero;

I just want to add that it seems the issue @Skarian mentioned above is introduced with v1.3.2. I tried switching back to v1.3.1 and the error was gone. Thanks for the including the code to resolve it!

mifi commented

Thanks for reporting @nathanielwood

I can confirm this is an issue with the cloneDeep addition in 1.3.2, I opened #37
This will be fixed in 1.3.3, so no longer need to use .default on imported json after that

maybe use something like merge-anything? that seems pretty stable and doesn't introduce as many security risks as lodash. I'm happy to pr this in if it helps

mifi commented

this one? https://github.com/mesqueeb/merge-anything
I believe it is not the same as cloneDeep, or is it? I think lodash.cloneDeep is quite safe as it is just a single function, and it has a lot more downloads/users:

there are high security warnings for anything lodash due to the way it works (https://snyk.io/vuln/SNYK-JS-LODASH-450202). its going to make it harder for consumers if you use lodash, since they will constantly have to do updates. we've found this on virtually every project that uses lodash. if you check out the pr I raised (#40), everything still works fine.