Open wujekbogdan opened 4 years ago
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:
npm run build
./build/player/lottie.js
file and put it in my project dir.lottie.js
in my WebWorker modulemount
option to false
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 Workersmount()
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:
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.
I use webpack together with GoogleChromeLabs/worker-plugin
// 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);
});
// register-worker.js
export default (callback) => {
addEventListener('message', ({ data }) => {
postMessage(callback(data));
});
};
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,
}
});
});
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:
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.
@bodymovin this would be useful for me to too. I'm using Lottie with PixiJS and don't want the Lottie renderer to render to the DOM. I'm just creating a WebGL texture to use as a resource for Pixi.
on the last release, I've added a lottie_worker player that delegates all js execution to a worker and only transfers to the main thread changed on the svg DOM. I think there are several things that could be extended from this work, but still not sure how it would integrate with other needs. @wujekbogdan would that work for your scenario?
Thank you.
I didn't have a look at it yet, but from your description it looks like you implemented Web Workers support in the library - the lib spawns a Web Worker on it's own.
It might work well although It's not exactly what I requested for. My original request was to make Lottie Web Workers enabled so that devs could offload Lottie tasks to the Web Worker thread on their own using their own Web Workers implementation (e.g. "raw" Web Workers or tools like comlink
or workerize-loader
.
Anyway, I'll give it a try and post a comment here.
since web workers only can transfer structured-clonable type of objects, it's not that straightforward to initiate lottie on a web worker and then run it on the main thread, regardless of its dependencies on window and document. https://developer.mozilla.org/en-US/docs/Web/API/Web_Workers_API/Structured_clone_algorithm I did two explorations so far:
lottie.useWebWorker(true)
before loading the animation)I still don't see different way of doing what you originally requested. The first exploration seems closer to your suggestion, but it almost makes no difference in the end.
Hi @bodymovin
Can you add some documentation and examples on how to use Web Workers on v5.8.1?
Any updates ? How to init Lottie in a web worker Nextjs?
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 whilelottie.loadAnimation
method requires a reference to a DOM node to be passed as thecontainer
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