17
votes

I'm using a CSS grid system which is based upon percentages. I have a grid with 4 columns, each 25% of the page's total width. I output my image tags inside of each "25% cell" like this:

<img src="foo.jpg" style="max-width:100%" />

As the browser resizes, the images also resize to fill in 100% of each 25% cell. The browser picks a height, as if I had put "height:auto" (which is implicit when omitted).

Now I want to add lazy loading capability to this. The problem is that before the images are loaded, their height on the page is unknown. The browser has to download the image & observe its aspect ratio, and calculate a height for it. Prior to this, all the images have a height of 1px. Since every image has a height of 1px, they are all considered as "within the viewport" and are immediately loaded.

Currently I have a proof of concept where prior to outputting the img tag, I calculate the images aspect ratio on the server, and output in a data attribute:

<img src="foo.jpg" style="max-width:100%" data-aspect="1.7742" />

Then, upon the event "document ready", I loop through every image and set a fixed 'height' value in pixels prior to lazy loading:

$('img').each(function() {
        var img = $(this);
        var width = img.width();
        var ratio = img.data('aspectratio');
        var height = width / ratio;
        $(this).css('height', height+'px');
    });

This seems to be working, in the sense that it no longer loads all the images at the same time, but only loads images as I scroll.

However, it seems like it could cause new problems, like the images becoming stretched as the user resizes the browser. I would have to switch the 'height' back to 'auto' when a callback fires for lazy loading having completed. That would take care of images the user sees - but the images below the fold would still have an improper 'height' value upon the browser being resized. Every time the browser is resized, I would have to iterate all images that were previously below the fold, measure their updated width, read their aspect ratio, and update the new height, and then retrigger lazy loading to handle anything that is now above the fold. If I don't do this, loading could be triggered too early or too late due to those images having the wrong height value.

My question is, is there any other ways to lazy load images with unknown heights, other than the exact method I've described here, and what ramifications would this have? Is there any downside to my method, other than it being a pain to program?

4
Looks like you've nailed it. The browser only has two ways to know an image's dimensions: to download it, or to have it specified in HTML.Blazemonger
Thanks for the reassurance. I was also envisioning some sort of approach where it lazy loads the first 4 images, then blocks until they are completed, and then reassesses the new positions of the remaining images. Only problem is the blocking would make things slower than without lazy loading in the first place.Josh Ribakoff
Ok I just became aware of the padding-bottom trick: thisisthat.co.uk/notebook/… andmag.se/2012/10/…Josh Ribakoff
Write it up as an answer to your own question.Blazemonger
Are you able to put up the code where you calculate to the aspect the ratio on the server please? This is the best solution I have found to this issue but can't find how to do thisJordan

4 Answers

5
votes

I had a similar problem recently, combining Isotope with Lazy Load in a responsive layout. Isotope determines the layout based upon the width and height of the images when the page is loaded, so initially, the items were all overlapping because Isotope wasn't calculating the correct size.

To make sure the placeholder items were saving the space, I used the padding-bottom trick you mentioned: http://thisisthat.co.uk/notebook/2013-10-07-lazy-loading-responsive-images (Though it may have not been that exact post.) Here's my markup:

    <article class="portfolio-item">
        <a class="portfolio-link" href="img/gallery/thumb90.jpg" style="padding-bottom: 66.2%">
            <div class="portfolio-image-wrapper">
                <img class="portfolio-image" data-original="img/gallery/thumb90.jpg" width="1000" height="662">
            </div>
            <div class="portfolio-text">
                <h1 class="portfolio-item-name">
                    <span href="#" class="icon" data-icon="e"></span>
                    <span class="portfolio-item-tags">Bridals</span>
                </h1>
            </div>
        </a>
    </article>

That's probably more involved than you need (as the entire .portfolio-text div is an overlay which has quite a bit of styling going on). The real key was just in calculating the bottom padding based upon the width and height of the image (which I did in the template with PHP) and setting that as the padding-bottom of the item that I wanted to save the space.

5
votes

Here's the more elegant solution, from the comments. It still requires writing the aspect ratio server side, but with a wrapper div:

<div class="lazy"><img src="foo.jpg" style="max-width:100%" data-aspect="0.75" /></div>

Then with JS I give the wrapper div a padding-bottom:

$('div.lazy').livequery(function() {
    var c = $(this);
    var r = c.data('ar');
    c.css('padding-bottom', r * 100 + '%');
});

This gives the div the exact dimensions that the img will eventually consume. I then use the following LESS to load the image within the area the padding consumes:

div.lazy {
  max-width:100%;
  position:relative;
  img {
    position:absolute;
    width:100%;
    height:100%;
  }
}
3
votes

Even cleaner:

  1. Set height and width of images to an arbitrarily-large number (like 2000px x 1000px)
  2. Apply the following CSS to each of the desired images (perhaps via a shared class):
    max-width: 100% and height: auto
  3. Smile wide :)

Credit for this approach goes to Github user dryabove, given in this Github issue

1
votes

if you have images with height and width props (WordPress's default) with loaded 1x1px gif in src - by default in some plugins (looking at you - wp-smush) then just plug this little beast on docready event in your script and it will auto-fix nasty vertical jumping of image when lazy loading ,, I know this is old post, but this is I believe modern js solution:

$('.lazyload').each(function(i,j){
    var h = $(j).attr( 'height' );
    var w = $(j).attr( 'width' );
    var at = `data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 ${w} ${h}'%3E%3C/svg%3E`;
    $(j).attr('src', at);
});