tvler/lazy-progressive-enhancement

Implementation of laziness

agarzola opened this issue · 20 comments

Hi! I was searching for progressive enhancement approaches to lazy loading and found your answer on Stack Overflow intriguing. This looks like a more standards-compliant solution, but I think the lazy component is missing.

If I’m reading your code correctly, this script by itself —i.e. loadMedia()— would still result in the browser downloading every image on the page all at once, instead of waiting for one event or another to fire. I imagine the intended use is for developers to create their own event-listening function that would then call loadMedia('#id') whenever img#id approaches the viewport. Is this correct?

In any case, are you planning on implementing a default listener of some sort, such that loadMedia() by itself would actually result in each image loading only as viewport scrolling approaches it?

tvler commented

Hi! Thanks for reaching out.

Having a loadMedia('scroll') type of feature would be good, but there are so many variable things that can happen to a page to affect an image's page height that I think developers should implement that themselves. For example if an element is resized or deleted, which causes an image's position to change, there's no JavaScript event for me to hook that repositioning logic into.

I think I like that this library right now is just a manual lazyloader, it keeps the code super clean. But I'll take a look at how the major lazy loaders calculate height. If it looks pretty stable I'll implement it for this. 👍

tvler commented

Keeping this issue open until I do more research

Thanks for the quick reply! I hear what you’re saying and figured that would be the reason for keeping it so barebones. This is a question where it is certainly better to err on the side of a conservative approach, so kudos for that.

My question was more about gauging your intent than anything else, since activity on this repo is still pretty recent and your SO answer implies you’re still working on it. I’d be happy to contribute where useful (be it research, discussion, or code), as I’m all for solutions that facilitate advanced functionality without breaking the experience for less-capable browsers.

Thanks again!

tvler commented

Cool! A good way to help right now would be to aggregate links on how other lazyloaders achieve this feature and post them in this issue, I was going to do that after work.

Feel free to start building a scroll functionality, but if the standard way to do it is super hacky I'd be a little uncomfortable bloating up the code.

Right now I'm thinking that running something like this

var ele = document.getElementById("yourDiv");
var x = 0;
while(ele){
   x += ele.offsetTop;
   ele = ele.offsetParent;
}

via http://stackoverflow.com/questions/14014974/how-to-get-height-from-div-to-top-window-of-browser-by-js

on every scroll event for every image element would be a pretty reasonable approach.

Something like that can be expensive when fired on an 'scroll' event, though. Especially when applied to dozens or hundreds of images. I’ll look into any solutions that might be available for listening from a viewport perspective, e.g. on scroll: calculate viewport position on the document and detect any noscript elements (preferably w/a special identifier) close to it. How to do that second step might not actually be feasible, but I’ll see what I can find.

tvler commented

Sounds good!

Maybe there can be another exposed function like calcScroll, which a developer can manually hook to their JavaScript functions which will reposition img elements. That way the height calculation I posted above won't need to fire on every scroll event.

I'll look into it more later today, thanks again @agarzola!

The function proposed in this S.O. answer looks like an interesting way of determining if an image is close to the viewport, with some modifications. The problem of when to fire that still remains, as attaching it to the 'scroll' event still seems like a deathtrap to me. To address that, I’ve been thinking about setInterval() a lot today.

Hear me out here: What if, on page load, we set an interval for each image on the document that we want to be lazy loaded (passing a user-defined selector to querySelectorAll to find those noscript elements). For each one, we fire off a setInterval(checkPos, 300) function. checkPos() would run a modified version of the above linked function, doing nothing if the image is found to be too far from the viewport.

If checkPos() finds this image to be in the viewport, it does two things: clear the interval for this image and invokes your existing loadMedia() function, passing to it the noscript element itself. That keeps it entirely decoupled, which might actually be a case for keeping it outside the scope of this project. In any case, I’ll think about it some and try to come up with some code to test.

OK, so here’s a rough & dirty implementation which seems to have no significant performance issues, though I wouldn’t call myself especially qualified to make that claim. First I create 1000 noscript elements, each with an img element pointing to a different source to ensure we’re not getting a cached resource (fakeimg.pl might hate me for this).

For each noscript, we create a placeholder image (tempImg) and attach some dataset properties (an imgId and an interval). That interval runs checkPos(), which in turn determines whether the current image is visible in the viewport. If it isn’t, nothing happens; if it is, then we clear the interval, invoke loadMedia(noscriptEl) and remove the temp image. (A more proper way to do it would be to pass something like tempImg.remove.bind(tempImg) as a second argument to loadMedia(), but in this example, because I’m dynamically generating the images via JS, they get fetched immediately. I don’t see a way around this, but it’s beside the point; this exercise is about exploring ways of invoking loadMedia() on a per-image basis.)

The big caveat here is that generating and displaying a correctly-sized temp image is necessary with this method. But that’s a problem every lazy loader will always have.

this article talks about your concerns with calling too many scroll events, offers a few solutions, might be helpful: https://css-tricks.com/debouncing-throttling-explained-examples/

tvler commented

The proof of concept you worked on is great, it's super exciting that someone else is interested in this project.

Work's been taking up most of my time this week, so I'll get to implementing a default 'scroll' setting this weekend probably.

tvler commented

Thanks @raglannyc for the link

tvler commented

The big caveat here is that generating and displaying a correctly-sized temp image is necessary with this method. But that’s a problem every lazy loader will always have.

I think we can just say that the image should have the dimension attributes set (or the dimensions set in CSS) for best results. It's already a "best practice" to specify image size so there's no scroll jank on image load.

Hi! Work ate up the rest of my week as well.

I agree that applying the same rules that apply to the real image is a good idea, but that‘s not always a hard value. Sometimes (most of the time, in my experience) it’s a relative value, like a percentage of its parent’s width or whatever the image’s natural size might be, with a max-height/width. In those cases, unless the temp image has the same proportions as the real image, I see no effective way to avoid scroll jank on image load.

That’s a very informative writeup, @raglannyc. Thanks for sharing that! Throttling sounds like the way to go if we were to listen from a scroll perspective. There’s something I like about attaching an interval to each image element and letting each one figure out on its own (so to speak) whether it’s time for it to load: The fact that it allows us to respond to events outside of scrolling. Adding/removing content via XHR or resizing the viewport; both are events that can affect the position of an image we want to lazy-load in relation to the viewport.

Rather than chasing any number of events that might cause an unloaded image to approach the viewport, it seems to me more robust to periodically inspect each image’s position relative to the viewport and act when appropriate.

Speaking of new content added in via XHR, a MutationObserver might come in handy as a way to automatically attach the interval to any new noscript elements loaded after the fact.

tvler commented

Cool, WICG is implementing an intersection observer to save developers from element visibility implementation hell ; )
https://github.com/WICG/IntersectionObserver

Looks like it's well on its way to become a standard browser API: https://www.chromestatus.com/feature/5695342691483648

That’s pretty great! I had no idea.

tvler commented

I've added a scroll option to the loadMedia function:

loadMedia (
   element,
   onload,
   scroll
)

element: CSS String | NodeList | Element

onload: Function (optional)

scroll: Boolean (optional) – loads image when visible

You can check out an example of it on the project's new website:
http://tylerdeitz.co/lazy-progressive-enhancement/

Thanks!

Great work! Loving the site. 👍

tvler commented

Thanks : )

I also thanked @agarzola and @raglannyc for contributing to this issue at the bottom of the readme.

💃