tuupola / lazyload

Vanilla JavaScript plugin for lazyloading images
https://appelsiini.net/projects/lazyload/
MIT License
8.76k stars 2.23k forks source link

Improving scrolling performance with high failure_limit #201

Open saucecontrol opened 10 years ago

saucecontrol commented 10 years ago

Howdy, Mika

I've been using your lazyload plugin for a long time, and it has worked great for me. I wanted to start by thanking you for sharing it. So... Thanks!

I ended up having to make some modifications to your plugin for a project I was working on, and I figured I'd share them in case you'd like to incorporate any of them. I did a pretty major overhaul of the code, so I'm not sure I'd be able to separate all the feaures and do individual manageable PRs for them. I did try to keep my commits small and well-documented, though, so I'll just give a rundown of the major changes here to supplement what's in my fork.

https://github.com/saucecontrol/jquery_lazyload/commits/master

The biggest change I made was a rewrite of the core measurement logic, in the first commit on that fork. I found that on pages with a large number of images and a high failure_limit set (a requirement on my project), the processing of the scroll event handler slowed too much to be usable.

I created a jsperf (actually a revision of one of yours) showing a basic comparison between the new and original logic. For the comparison, I stripped the 'box' class out of my revised plugin and used it to create mirrored helper methods and jQuery pseudo selectors so they could be compared in the same page. Just using the updated logic with the :in-viewport pseudo resulted in ~10x performance gain in the browsers I tested.

I also revised the update() function in the plugin to measure the container only once per call rather than once per child element. This can yield up to another 2x performance gain, depending on the container (window measurement is less overhead, so less gain for that).

In addition, I addressed some of the event and DOM leaks to make the plugin behave better when using it with AJAX-loaded content.

I did a couple of profiles in the Chrome developer tools illustrating some of the problems and their fixes. For these profiles, I used the default plugin settings with the exception of failure_limit, which was set to 400. I loaded a page with 387 images on it, scrolled to the bottom, forced 2 garbage collections, and scrolled back to the top. I repeated the process with both v1.9.3 and with my modified copy. In both cases, all the images were already in the local cache, so all processing is from the plugin and the painting from the scroll.

Memory/Objects Before: beforememory

Memory/Objects After: aftermemory

Frames Before: beforeframes

Frames After: afterframes

Events Before: beforeevents

Events After: afterevents

About the major changes

I updated the measurement logic to use getBoundingClientRect (gbcr) where possible. gbcr always returns coordinates relative to the window viewport, regardless of document hierarchy or position: styles. This allows any two sets of coordinates to be compared directly, since they always have the same origin. When comparing to the window, you simply compare with a rectangle starting at 0,0 and going to window width,height. This is the simplest possible logic and because gbcr returns both position and size in the same call, it is incredibly fast.

For clients not supporting gbcr, I fall back to the jQuery logic used in the original version, with only two differences. 1) I use outerWidth() and outerHeight() instead of width() and height() for consistency with the gbcr measurements, which include padding and borders. 2) I force anything with display:hidden or that is detached from the DOM to return an empty rectangle. Again, this is for consistency with gbcr. It also addresses most of what the original skip_invisible logic does. More on that...

Elements that have display:hidden or that have been detached from the DOM have neither position nor size, so their relationship to anything else is undefined.

getBoundingClientRect() reports this correctly by returning an empty rectangle (0,0,0,0) in either of these cases. jQuery's offset() method will return an origin of 0,0 for either case, but if an element is only display:hidden, the width() and height() methods may return non-0 values depending on how they are styled.

The :visible check in the old logic handles either of those situations correctly but will also lump in elements that have no size, even if they are part of the layout and have position. This is the source of most of the complaints related to images not loading in webkit browsers if the size isn't set explicitly. In my testing, even when an image is in the local cache, webkit will report its size as 0x0 at the time of document.ready. It may then have a size as little as 1ms later, once it has been retrieved from the cache. My updated logic allows an image with no size but with a position to be considered for triggering, which should eliminate this problem. The only downside to that logic is that if the page layout consists of nothing but images with height and width of 0, they may all fit in the viewport initially and may all trigger. In reality, that's pretty unlikely, so I think it's a good compromise... showing more rather than less.

Anyway, if there are any questions I can answer or anything I can do to make my changes easier to incorporate, let me know. And thanks again for sharing and maintaining this plugin.

baijunjie commented 9 years ago

I saw your code, feel your idea is very good, but the code has a few obvious mistakes, don't know whether you test your code?

options.threshold === undefined qq 20150422164537

this.empty always equal to the false qq 20150422164625

Should be modified to: qq 20150422160928 qq 20150422161021

saucecontrol commented 9 years ago

Quite right. Thanks for pointing those out. I always use a threshold when I use this plugin, so the value is always present in both options and settings for me. I've updated my fork with your suggested changes.