<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><!--[if IE 9]><!--><noscript data-lazy-img><!--<![endif]--></code></li>
<li>Add after image: <code><!--[if IE 9]><!--></noscript><!--<![endif]--></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 & 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);
}
});
This Pen doesn't use any external CSS resources.