<h1>Lazy loading images</h1>
<p>Main purpose of lazy loading: save <strong>your</strong> bandwidth. Side effect: user's initial page load time might reduce. However user may also see images flashing as they are loaded.</p>
<h2>Challenges:</h2>
<dl>
  <dt>Should work JavaScript disabled</dt>
  <dd>Just without lazy loading of course. Also means Google and other search engines can access the image.</dd>
  <dt>Should have short syntax</dt>
  <dd>Many solutions I've seen seem to easily become quite verbose with their HMTL. Especially if you want to support the above.</dd>
  <dt>Should work nicely in mobile</dt>
  <dd>I don't want my iPad to load them with extra 2 seconds delay when on 4G!</dd>
  <dt>Don't be slow!</dt>
  <dd>Some solutions seem to be very slow on initial page load: in my tests with mere 50 images jQuery JAIL plugin spent 150 ms setting stuff up!<br /><small>Yes, that is a lot. It would double the page JS init time on my production site.</small></dd>
  <dt>Should account for both vertical and horizontal scrolling.</dt>
  <dd>Some solutions only account for vertical scrolling.</dd>
  <dt>Should work on Internet Explorer 8+</dt>
  <dd>The smaller you make it and the more feature requirements you add the more likely you are going to run into compability barriers.</dd>
</dl>

<noscript data-lazy-img><img alt="Test" title="FYI: This does not work on IE8 and below" src="http://i1-news.softpedia-static.com/images/news2/Critical-Out-of-Band-Patch-for-Internet-Explorer-8-2.jpg" height="417" width="417" /></noscript>

<!--[if IE 9]><!--><noscript data-lazy-img><!--<![endif]--><img alt="Test" src="http://placehold.it/183x360" height="360" width="183" /><!--[if IE 9]><!--></noscript><!--<![endif]-->

<!--[if IE 9]><!--><noscript data-lazy-img><!--<![endif]--><img alt="Test" src="http://placehold.it/267x300" height="300" width="267" /><!--[if IE 9]>--></noscript><!--<![endif]-->

<!--[if IE 9]><!--><noscript data-lazy-img><!--<![endif]--><img alt="Test" src="http://placehold.it/193x156" height="156" width="193" /><!--[if IE 9]>--></noscript><!--<![endif]-->

Image that fails to load:
<!--[if IE 9]><!--><noscript data-lazy-img><!--<![endif]--><img class="fails" alt="Test" src="#obviously-is-not-an-image" height="300" width="300" /><!--[if IE 9]>--></noscript><!--<![endif]-->

<h2>The Requirements</h2>
<ul>
  <li>Add before image: <code>&lt;!--[if IE 9]&gt;&lt;!--&gt;&lt;noscript data-lazy-img&gt;&lt;!--&lt;![endif]--&gt;</code></li>
  <li>Add after image: <code>&lt;!--[if IE 9]&gt;&lt;!--&gt;&lt;/noscript&gt;&lt;!--&lt;![endif]--&gt;</code></li>
  <li>Image must have width and height attributes set (as always with lazy loading).</li>
  <li>And that is all that is into it in HTML. Rest is JavaScript.</li>
</ul>

<h2>The Good</h2>
<p>Shortest syntax I know of that also supports JS disabled.</p>
<p>Answers all the challenges in a positive manner.</p>

<h2>The Bad</h2>
<p>There can be a small delay after page is first rendered and JavaScript execution which means page layout might be re-calculated when noscript elements are replaced with images.</p>
<p>Also doesn't support responsive loading if you fancy that.</p>

<h2>The Weird &amp; Ugly</h2>
<p>Internet Explorer Conditional Comments: IE 8 and below don't allow accessing contents of noscript elements.</p>
<p>With some simple conditional comments we can hide noscript element tags from those IEs so they work as if JS is disabled.</p>
<p>But then we get no lazy loading.</p>
/* just to add distance between images */
img {
  border: 1px dotted #ccc;
  display: block;
  margin: 20em 10em;
}

img.fails {
  border-color: red;
  margin-top: 0;
}

/* a little bit of style */
html {
  padding: 2em;
}

dl {
  margin: 1em;
}

dt {
  display: list-item;
  font-weight: bold;
}

dd {
  font-size: smaller;
  margin: 1em;
}
$(function() {
  var $window = $(window),
      images = [],
      imagesToBeLoaded = 0,
      i,
      src;

  function throttle(func, wait) {
    var timeout;
    return function() {
      var context = this, args = arguments;
      if(!timeout) {
        timeout = setTimeout(function() {
          timeout = null;
        }, wait);
        func.apply(context, args);
      }
    };
  }
  
  function inViewport($el) {
    var top = $window.scrollTop(),
        left = $window.scrollLeft(),
        bottom = top + $window.height(),
        right = left + $window.width(),
        offset = $el.offset(),
        thisTop = offset.top,
        thisLeft = offset.left,
        thisBottom = thisTop + $el.outerHeight(),
        thisRight = thisLeft + $el.outerWidth();

    return !(
      bottom < thisTop ||
      top > thisBottom ||
      right < thisLeft ||
      left > thisRight
    );
  }

  // throttle so we don't do too many calls
  var lazyScroll = throttle(function() {
    // have all images been loaded?
    if(imagesToBeLoaded > 0) {
      for(i = 0; i < images.length; i++) {
        // data is there if nothing has been done to it
        src = images[i].data('src');
        // see if the image is in the view
        if(src && inViewport(images[i])) {
          // create a nice closure here
          (function(img, src, i, $img) {
            img.onload = function() {
              console.log('Loaded ' + i + ' ' + img.src);
              $img.attr('src', img.src);
              imagesToBeLoaded--;
            };
            img.onerror = function() {
              console.log('Could not load ' + i + ' ' + img.src);
              imagesToBeLoaded--;
            };
            // important to remove this to avoid duplicate calls
            $img.removeData('src');
            // start loading the image
            img.src = src;
          })(new Image(), src, i, images[i]);
        }
      }
    } else {
      // cleanup
      images = void 0;
      // why keep listening if there is nothing to listen
      $window.off('resize scroll touchmove', lazyScroll);
      // all images are loaded
      console.log('Unloaded event listener');
    }
  }, 50);
  
  $('noscript[data-lazy-img]').each(function() {
    var $this = $(this),
        $img = $(this.innerText || $this.text()).filter('img');
    // make sure we got something
    if($img.length === 1) {
      // remember the real image
      $img.data('src', $img.attr('src'));
      // use a blank image
      $img.attr('src', 'data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7');
      // cache a reference
      images.push($img);
      // replace noscript element with the image
      $this.replaceWith($img);
      imagesToBeLoaded++;
    }
  });
  // only add if we need it
  if(imagesToBeLoaded) {
    lazyScroll();
    $window.on('resize scroll touchmove', lazyScroll);
  }
});

External CSS

This Pen doesn't use any external CSS resources.

External JavaScript

  1. //cdnjs.cloudflare.com/ajax/libs/jquery/2.1.3/jquery.min.js