dkern / jquery.lazy

A lightweight, fast, feature-rich, powerful and highly configurable delayed content, image and background lazy loading plugin for jQuery & Zepto.
http://jquery.eisbehr.de/lazy
Other
1.02k stars 237 forks source link

lazy creates/destroys scroll handlers even if there are no images left to load #197

Closed na-oma closed 2 years ago

na-oma commented 5 years ago

Hi, great plugin! It's nice not having to rely on other plugins' rudimentary lazy-load support.

We are using lazy together with https://flickity.metafizzy.co/ to scrolls through containers which contain lazy images (rather <picture>s).

Our setup (for ALL lazy images on the page, not only in flickity):

    $('.lazy').Lazy({
        effect: "fadeIn",
        effectTime: 500,
        threshold: 0
    });

Handling flickety scroll events potentially revealing lazy images (calls lazy for all lazy images in the whole flickety container):

$('.main-carousel').on('scroll.flickity', function (event, progress) {
    $(event.target).find('.lazy').lazy('update', {useThrottle: true});
});

It works like a charm, but is it even correct to instantiate lazy multiple times like this???

The question: When flickity fires a scroll event, and all lazy images in this container have already been loaded by lazy, lazy still creates and instantly destroys an instance (_lazyLoadItems() => instance.destroy() This is not a problem per se, but before lazy checks if there are any elements to handle, it already registers a scroll handler $(config.appendScroll).on('scroll.' + namespace + ' resize.' + namespace, events.e); and unbinds it in destroy: $(_config.appendScroll).off('.' + _namespace, _events.e); $(window).off('.' + _namespace); The unbinding (jQuery remove) is especially taxing: it takes 10% of cpu (chrome profiler, on desktop!) while scrolling.

Is it possible to alter the code to only bind/unbind events, when there are actually any items to be loaded?

Alternatives:

P.S. Another potential problem: In the same case(no elements to load), _lazyLoadItems triggers unbinding the events via instance.destroy() and only after the events are unbound, the new events are bound inside _initialize!

dkern commented 5 years ago

Because I'm not sure about your call, could you please update your code like below and tell me if this works better? Thanks you.

var instance = $('.lazy').Lazy({
    effect: "fadeIn",
    effectTime: 500,
    threshold: 0,
    chainable: false
});

$('.main-carousel').on('scroll.flickity', function() {
    instance.update(true);
});
na-oma commented 5 years ago

Sorry for the late response. I thought that getting a lazy instance via chainable: false and data("plugin_lazy") is exactly the same, but one is leaking memory rapidly and the other works:

1. Getting instance via chainable false :rage: :rage: :rage: produces memory leaks(50MB/min), maybe the instance is not properly cleaned up? and many unnecessary remove function calls. See Chrome Profile/Memory Codepen

$('#testcontainer .lazy').Lazy();

setInterval(() => {
  //leaks memory even if you don't call instance.update
  var instance = $('#testcontainer .lazy').Lazy({ chainable: false });
  instance.update(true);
}, 1);

2. Getting instance via plugin_lazy works :+1: Codepen

$('#testcontainer .lazy').Lazy();

setInterval(() => {
  var instance = $('#testcontainer .lazy').data("plugin_lazy");
  instance.update(true);
}, 1);

3. Saving global instance reference once works :+1: Codepen

var instance = $('#testcontainer .lazy').Lazy({
    chainable: false
});

setInterval(() => {
  instance.update(true);
}, 1);

Is there really such a difference between the methods 1 and 2?

Of course nobody would use setInterval(_, 1). But doing the call in a scroll handler generates the same amount of mem leaks and CPU waste.

dkern commented 5 years ago

Using chainable doesn't consume any memory. In fact, it should be a bist faster then using data too. The problem simply is, that you did it wrong. In your first example you create a new Lazy instance with all elements of the selector and update is directly.

I'll repeat: You create a whole new instance of the plugin every 1ms!!! What do you expect?!

To correct approach is your third example. As you see in my post above. Create an instance once and reuse it.

na-oma commented 5 years ago

I know I used it wrong, I got that now :) But there is still a memory leak when creating an instance with no element in the selector.

What do you expect?!

I expect the instances to be properly cleaned up, even if I create 1000s of them: auto destroy is true by default and there are no more images to be loaded.

In detail:

Maybe I misunderstand what autoDestroy means. I expect it to clean the whole instance as if it was never created.