19

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?

Josh Ribakoff
  • 2,948
  • 3
  • 27
  • 26
  • 1
    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 May 01 '14 at 21:27
  • 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 May 01 '14 at 21:33
  • 1
    Ok I just became aware of the padding-bottom trick: http://thisisthat.co.uk/notebook/2013-10-07-lazy-loading-responsive-images http://andmag.se/2012/10/responsive-images-lazy-load-and-multiserve-images/ – Josh Ribakoff May 01 '14 at 21:46
  • 1
    Write it up as an answer to your own question. – Blazemonger May 01 '14 at 21:47
  • 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 this – Jordan Oct 21 '14 at 03:16
  • @Jordan I would start a new question, as this will vary based on language used on the server. For example if you use PHP, you can use the imagick class to get the width & height, then divide them to get the aspect ratio expressed as a decimal.... – Josh Ribakoff Oct 22 '14 at 18:41
  • Can the image dimensions be calculated on server side? If so, perhaps my other answer might be handy https://stackoverflow.com/a/65978540/2987689. – Artur INTECH Jan 31 '21 at 11:44

4 Answers4

5

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.

jpsingleton
  • 101
  • 3
  • 1
    We're actually using isotope too on some pages! I fire an interval every 1000ms while the images are still loading that tells isotope to redraw. This way the images keep shuffling around while they are loading, instead of loading in 1 big column and then being arranged at the end. Do you do the same, and can you elaborate on how the JS that "glues" it all together works for your project? – Josh Ribakoff May 01 '14 at 22:37
5

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%;
  }
}
Josh Ribakoff
  • 2,948
  • 3
  • 27
  • 26
3

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

Isaac Gregson
  • 1,999
  • 1
  • 21
  • 31
  • 1
    If you want a placeholder image e.g. gray background with spinner (or you want your design to hold the right structure with nothing visible), and the image hasn't been loaded yet, `height: auto` will not work. There is no image information to calculate the right ratio of height to width... – Chadwick Meyer Jan 28 '16 at 23:09
  • no luck for me on Chrome 57. Images unpredictably overlapping vertically. – evanmcd Apr 16 '17 at 21:34
  • 4
    This will not work if the images are not the same ratio – Drenai May 16 '18 at 10:36
1

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);
});
Kresimir Pendic
  • 3,597
  • 1
  • 21
  • 28