Lottie-web: [feature request] Make it possible to initialize a lottie instance without giving a reference to a DOM node

Created on 11 Nov 2019  ·  18Comments  ·  Source: airbnb/lottie-web

Sometimes it takes very long for the animations to load. While the animations are being loaded the main thread is blocked by Lottie for a significant amount of time - even hundreds of milliseconds on Core i7 CPU.

I was trying to offload Lottie initialization to a Web Worker - I wanted to call lottie.loadAnimation from within a Web Worker thread. But it turned out It is not possible because WebWorkers don't have access to DOM while lottie.loadAnimation method requires a reference to a DOM node to be passed as the container argument.

So I wonder is it possible to make the loadAnimation method DOM agnostic? It would be great if we could initialize the Lottie instance in a Web Worker thread, return it, and then mount on a DOM element.


PS
I know there's a canvas rendered, but Offscreen Canvas, which is supported by WebWorkers has still very poor browsers' support: https://caniuse.com/#feat=offscreencanvas

Most helpful comment

I've described the reason in my first post in this topic: https://github.com/airbnb/lottie-web/issues/1860#issue-521131207


Lottie initialization process is a very CPU-heavy process, in case of complex animations the main thread gets busy for hundreds of milliseconds, even up to a 1s and more (depending on the animation complexity and the CPU). JS is single-threaded, because of this, while the animation is being processed by lottie-web, the whole application's UI is frozen (all the animations are stopped, hovers don't work, events are being triggered, etc). It gives a very bad user experience.

WebWorkers run in a separate thread, so offloading the init process to a separate WebWorker thread would make the init process way smoother.

So basically, this is a very standard use case for WebWorkers.

All 18 comments

Hi, I'll think about what could be done about this.
But do you have an example of a slow loading animation?
Also keep in mind that you can pass progressiveLoad as true to the renderer options which will load animations gradually while layers are needed. This should distribute loading across multiple frames.

@bodymovin

Hi, I'll think about what could be done about this.

That's great, I really appreciate it.

But do you have an example of a slow loading animation?

This problem is not specific to a particular animation. Loading any large animation file (by large I mean > ~200kb) block the main thread for a significant amount of time.

Do a simple test:

console.time('lottie');
lottie.loadAnimation(json);
console.timeEnd('lottie');

Also keep in mind that you can pass progressiveLoad as true to the renderer options which will load animations gradually while layers are needed. This should distribute loading across multiple frames.

That's interesting, I'll check it out. It might be a good workaround. But still it would be great if the DOM reference wasn't required by loadAnimation.

I mean something like:

const lottieInstance = lottie.loadAnimation(json); // initialization part would be offloaded to a WebWorker
const $el = document.querySelector('#js--animation-container');
lottieInstance.mount($el)

Let's make sure that my issue was understood correctly. I'm not talking about the animation being laggy. After the animation is loaded it works perfectly. The issue I'm talking about affects only the inial load.

I'll think about it and try some things. Meanwhile can you check if progressiveLoad makes any difference?

I'll think about it and try some things.

Thank you!

Meanwhile can you check if progressiveLoad makes any difference?

progressiveLoad disabled.

219.840087890625ms
219.47412109375ms
228.85302734375ms
294.090087890625ms

progresiveLoad enabled

229.35302734375ms
248.43798828125ms
318.260986328125ms
218.35107421875ms

The numbers vary on each execution, but I can't see a significant difference. Sometimes progressiveLoad enabled is faster, sometimes the opposite.

can you share one of the animations?

I shared them with you via e-mail (the one that's visible on your user's profile). I don't want to share these resources publicly because they belong to the company I work for.

Hi, I've been doing some progress on this. Would you be willing to try it out?

Hi, I've been doing some progress on this.

Cool!

Would you be willing to try it out?

Yes, of course. Is there any branch I can pull to try it out?

branch is 76_mount_dom. It only works on the svg renderer for now.
You can get the player from the build folder:
https://github.com/airbnb/lottie-web/tree/76_mount_dom/build/player

you need to pass mount as false in the rendererSettings.
Once it's ready, you can call mount() on the animation instance.

If you try it out, let me know how it goes.
Thanks!

Thanks, I gave it a try, but I can't get it to work:

  • I cloned the repo and build the project running npm run build
  • I grabbed the ./build/player/lottie.js file and put it in my project dir.
  • I imported lottie.js in my WebWorker module
  • I set the mount option to false

Issues

  • When I import Lottie then I get an error message saying: Uncaught ReferenceError: window is not defined - it happens because lottie.js tries to access the window object in several place. It's not possible because the window object and its methods/properties are not available for Web Workers
  • The mount() method doesn't accept any arguments, it means that the DOM node needs to be passed as a container attribute to Lottie constructor (as it used to be). It won't work this way. The idea was that the code executed within the worker thread should be DOM-agnostic, it should return a Lottie instance to the main thread, where the Lottie animation is mounted on a certain DOM node. We can't pass a container property to the lottie constructor, because we can't pass it via postMessage to a Web Worker. postMessage won't allow for passing DOM node reference because web Workers don't have a DOM access.

Workers don't have an access to:

  • The DOM
  • The window object
  • The document object
  • The parent object

Source: https://www.html5rocks.com/en/tutorials/workers/basics/#toc-enviornment-features

PS
It's not an issue with the build because the library, used in a standard way (not in a WebWorker thread) works fine. The mount() method also works fine. The problem only applies to running it in as a Web Worker thread.


Here's the WebWorker code:

I use webpack together with GoogleChromeLabs/worker-plugin

A generic module that loads and promisifies all the workers in my app:

// web-worker.js
import isFunction from 'lodash/isFunction';
import ExtendableError from '@/models/ExtendableError';

export class WorkerFactoryError extends ExtendableError {}

export const WORKERS = {
  LOAD_LOTTIE_ANIMATION: () => new Worker('./load-lottie-animation.js', { type: 'module' }),
  // ... more workers here
};

/**
 * @param {String} msg - error message
 * @param {Function} reject - a reference to Promise.reject
 */
const throwException = (msg, reject) => {
  const error = new WorkerFactoryError(msg);
  reject(error);
  throw error;
};

/**
 * @param {Function} worker - a reference to a function that returns a new Worker instance
 * @param {*} postMessageData
 * @return {Promise<any>}
 */
export default (worker, postMessageData) =>
  new Promise((resolve, reject) => {
    if (!isFunction(worker)) {
      throwException(`worker is expected to be a function, instead got ${typeof worker}.`, reject);
    }

    const workerInstance = worker();

    if (!(workerInstance instanceof Worker)) {
      throwException(
        `worker callback is supposed to return a Worker instance, instead got ${typeof workerInstance}.`,
        reject
      );
    }

    const handleError = (e) => {
      workerInstance.terminate();
      throwException(e.message, reject);
    };

    const handleMessage = ({ data }) => {
      workerInstance.terminate();
      resolve(data);
    };

    workerInstance.addEventListener('message', handleMessage, false);
    workerInstance.addEventListener('error', handleError, false);
    workerInstance.postMessage(postMessageData);
  });

A tiny postMessage wrapper:

// register-worker.js
export default (callback) => {
  addEventListener('message', ({ data }) => {
    postMessage(callback(data));
  });
};

The lottie worker that returns a lottie instance.

// load-lottie-animation.js
import '@/3rd-party/lottie/lottie';
import registerWorker from './register-worker';

export default registerWorker((options = {}) => {
  const { lottie } = self;
  return lottie.loadAnimation({
    ...options,
    renderer: 'svg',
    rendererSettings: {
      mount: false,
    }
  });
});

Here's the Lottie initialization code

import webWorker, { WORKERS } from '@/web-worker';
import animation from './animation.json';

const loadAnimation = async (animationData, container) => {
  const lottie = await webWorker(WORKERS.LOAD_LOTTIE_ANIMATION, {
    animationData,
    container, // this element can't be here. It can't be sent via postMessage
    loop: false,
    autoplay: true,
  });
  lottie.renderer.mount(); // I was expecting to be able to do lottie.renderer.mount(container)
};

loadAnimation(animation, document.getElementById('#js--lottie'));

Hi, you are right, if you are initializing it on the webworker side, the container needs to be passed afterwards. I'll work on that next.
Regarding the window reference, it is actually not being used that much, but it does expect window to be available. I can perhaps try to handle it at the module level with an inner reference, but as a quick workaround, can you try declaring a global window variable before initializing the library?

Hi, you are right, if you are initializing it on the webworker side, the container needs to be passed afterwards. I'll work on that next.

Thanks.

Regarding the window reference, it is actually not being used that much, but it does expect window to be available. I can perhaps try to handle it at the module level with an inner reference, but as a quick workaround, can you try declaring a global window variable before initializing the library?

I added these lines at the very top of the player.js file in order to "mock" all the objects and methods that lottie needed to access.

// player.js
var window = {};
var document = {
  createElement: function() {
    return {
      getContext: function() {
        return {
          fillRect: function() {
            return {
            }
          }
        }
      }
    }
  },
  getElementsByTagName: function() {
    return {
    }
  }
};

I started with var window = {} and kept on adding the remaining ones until I reach a stage when the errors caused by missing methods/objects stopped showing up.

Now lottie.loadAnimation() returns an instance of the AnimationItem object, but this object can't be transferred from the WebWorker thread to the main thread via postMessage.

  // After mocking `window` and `document` objects the following piece of code executes without rasing any arror:
  const lt = lottie.loadAnimation({
    ...options,
    renderer: 'svg',
    rendererSettings: {
      mount: false,
    },
  });

  // I get an AnimationItem here
  console.log(lt);

  // But here where the `registerWorker` tries to send the data via `postMessage` I'm getting an error
  return lt;

After postMessage executes, I'm getting the following error:

Uncaught DataCloneError: Failed to execute 'postMessage' on 'DedicatedWorkerGlobalScope': function removeElement(ev){
        var i = 0;
        var animItem = ev.target;
        while(i<len) {
       ...<omitted>... } could not be cloned.

I'm afraid that this feature will require much more work. At the moment the whole lottie library is imported by the WebWorker - the main thread receives a complete, initialized lottie instance.
IMO the architecture should be broken into 2 modules:

  • The animation initializer module that would do all the expensive initialization work that is DOM-agnostic.
  • The player module that would receive the instance of the initialized object and run it on the main thread.

I'm thinking of something like this:

Main thread:

// main-thread.js
import webWorker, { WORKERS } from '@/web-worker';
import animation from './animation.json';
import { player } from 'lottie';

const loadAnimation = async (animationData, container) => {
  // Returns an initialized lottie animation
  const lottie = await webWorker(WORKERS.LOAD_LOTTIE_ANIMATION, {
    animationData,
    loop: false,
    autoplay: true,
  });

  // passes the animation to the player and mounts it on DOM
  player(lottie).mount(container);
};

loadAnimation(animation, document.getElementById('#js--lottie'));

WebWorker thread:

// load-lottie-animation.js
import { initializer } 'lottie';
import registerWorker from './register-worker';

export default registerWorker((options = {}) => {
  return initializer.loadAnimation(options);
});

@bodymovin

Is there any progress on this feature? Can we expect a solution to this problem in future releases of lottie-web?

also interested in this feature 👍

@wujekbogdan @dnix what are the use cases where you would need this?

I've described the reason in my first post in this topic: https://github.com/airbnb/lottie-web/issues/1860#issue-521131207


Lottie initialization process is a very CPU-heavy process, in case of complex animations the main thread gets busy for hundreds of milliseconds, even up to a 1s and more (depending on the animation complexity and the CPU). JS is single-threaded, because of this, while the animation is being processed by lottie-web, the whole application's UI is frozen (all the animations are stopped, hovers don't work, events are being triggered, etc). It gives a very bad user experience.

WebWorkers run in a separate thread, so offloading the init process to a separate WebWorker thread would make the init process way smoother.

So basically, this is a very standard use case for WebWorkers.

I have a use case for passing a canvas/2D context to a WebGL texture, without a DOM element. Appreciate your thoughts on this!

On May 9, 2020, at 9:15 AM, hernan notifications@github.com wrote:


@wujekbogdan @dnix what are the use cases where you would need this?


You are receiving this because you were mentioned.
Reply to this email directly, view it on GitHub, or unsubscribe.

@bodymovin

We are also stumbled with this issue. We are showing loading animation while initialising game level. FPS drops almost to 0 when lottie.loadAnimation starts.

Was this page helpful?
0 / 5 - 0 ratings

Related issues

Ipaulsen picture Ipaulsen  ·  4Comments

ochanje210 picture ochanje210  ·  3Comments

yannieyeung picture yannieyeung  ·  3Comments

hardy613 picture hardy613  ·  4Comments

phileastv picture phileastv  ·  3Comments