Using the viewport position to late-load content
Late load images or entire blocks of content dependent upon the page scroll position. The method depicted here is an incredibly lightweight vanilla JavaScript method which even displays content when scripting is unavailable.
A shout-out for Alex Whitlock and Sushma Varadaraj, for helping out with the pre-implementation testing.
Real world implementation now Live on Tesco Summer 2015.
Apologies for the flagrant copyright abuse Mr Lucas Disney lawyer sir, I'm desisting and ceasing as you press send, honest guv.
So how does it work?
Blocks to be late-loaded are encapsulated in <noscript class=idle-load> elements.
Essentially JavaScript replaces each of the tags with a <div class=idle-load-ON> as its parent container, the <div>, approaches the viewport.
Just how simple is this?
<div>
<noscript class=idle-load>
<h3>Obi Wan Kenobi</h3>
<img src="i/obi_wan_kenobi.jpg" alt="">
<p>A wise and skilled Jedi Master…</p>
</noscript>
</div>
Add the script.
// idleload.2.5.js - 592 bytes gzipped (1.01KB uncompressed)
var idleLoad=function(){function p(a,c){var b,d;c||(c=250);return function(){var f=this,e=+Date.now(),g=arguments;b&&e<b+c?(clearTimeout(d),d=setTimeout(function(){b=e;a.apply(f,g)},c)):(b=e,a.apply(f,g))}}function h(){var a,c;if(b)if(c=b.length)for(;c--;){if(a=b[c].getAttribute("data-"+k)||e,b[c]&&document.documentElement.clientHeight*a>=b[c].parentNode.getBoundingClientRect().top){a=b[c];var h=l,d=document.createElement(m);d.className=a.className.replace(f,f+n);d.innerHTML=a.innerHTML.replace(/</g,
"<").replace(/>/g,">");a.parentNode.replaceChild(d,a);h&&h(d)}}else window.removeEventListener("scroll",g,!1)}var b,e,k,m,f,n,l,g;return{init:function(a){document.addEventListener&&document.getElementsByClassName&&(f=a.elementClass||"idle-load",b=document.getElementsByClassName(f),n=a.onClass||"-ON",m=a.elementTo||"div",e=(e=0===a.offsetViewportBy?1E-4:a.offsetViewportBy)||1.5,k=a.idleAttribute||"idle",g=p(function(){h()},a.pollDelay),l=a.callbackFunc||!1,window.addEventListener("scroll",g,!1),
h())}}}();
Add configuration options (just before the </body> tag), amend to suit.
idleLoad.init({ // All options optional:
elementClass : 'idle-load', // (default) Class of <noscript> tags to replace.
onClass : '-ON', // (default) Added to the class name upon replacement.
elementTo : 'div', // (default) Element to replace <noscript>.
offsetViewportBy : 0.5, // Viewport: 0 top, 0.5 halfway, 1 bottom, 1.5 (default) half a screen below the viewport.
// May be overridden by a data-idle value on a noscript tag: data-idle='1'.
idleAttribute : 'idle', // (default) Data attribute to override offsetViewportBy, uses same values.
pollDelay : 250, // (default) How often polling to throttle occurs.
callbackFunc : function(){} // Call back function on noscript replacement.
});
Done. So simple a blathered Ewok could code right?
The full version is available in the source of the standalone demo.
Scripting - embrace the dark side
Here's a break down of the uncompressed script.
Use a closure to prevent polluting the global namespace, then declare the internal variables.
var idleLoad = (function () {
// Idle-load v2.5 - 15/06/2015 - © M.J.Foskett webSemantics 2015 - http://websemantics.uk/articles/idle-load/
// Delays the loading of images and content dependant upon scroll position
'use strict';
var previousTop = 0,
idles,
offsetViewportBy,
idleAttribute,
elementTo,
elementClass,
onClass,
callback,
throttleScrolling;
The replaceElement() function replaces one element with another, copying content and class. '-ON' is appended to the class.
Used here to replace a <noscript> with a <div>.
function replaceElement(el, elName, callback) {
var newEl = document.createElement(elName);
// append '-ON' to the class name thereby removing it from the live list idles[].
newEl.className = el.className.replace(elementClass, elementClass + onClass);
newEl.innerHTML = el.innerHTML.replace(/&lt;/g, '<').replace(/&gt;/g, '>');
el.parentNode.replaceChild(newEl, el);
if (callback) {
return callback(newEl);
}
}
The throttle() function prevents stressing the scroll event handler too much.
function throttle(func, wait, scope) {
// https://remysharp.com/2010/07/21/throttling-function-calls
// Updates every 250ms (default)
var last, deferTimer;
wait || (wait = 250);
return function () {
var context = scope || this,
now = + Date.now(),
args = arguments;
if (last && now < last + wait) {
clearTimeout(deferTimer);
deferTimer = setTimeout(function () {
last = now;
func.apply(context, args);
}, wait);
} else {
last = now;
func.apply(context, args);
}
};
}
The scrolling() function is called every 250 ms (default) while the page is scrolled. It needs to be efficiently managed. Micro optimisations should be used wherever they can.
As the viewport approaches each element a call to the replaceElement() function is made.
function scrolling() {
var scrollTop = window.pageYOffset,
offsetY,
i;
if (idles) {
i = idles.length;
if (i) {
while (i--) {
// In relation to the viewport when should this idle[i] appear?
offsetY = idles[i].getAttribute('data-' + idleAttribute) || offsetViewportBy;
// Are we there yet dad?
if (idles[i] && (document.documentElement.clientHeight * offsetY) >= idles[i].parentNode.getBoundingClientRect().top) {
replaceElement(idles[i], elementTo, callback);
}
}
previousTop = scrollTop;
} else {
// tidy like
window.removeEventListener('scroll', throttleScrolling, false);
}
}
}
The initialise(cfg) function checks the correct level of JavaScript is supported and reads the configuration settings into the scoped variables.
function initialise(cfg) {
// Test for required JavaScript support (IE 9+)
if (document.addEventListener && document.getElementsByClassName) {
// Read config values into scoped variables
elementClass = cfg.elementClass || 'idle-load';
idles = document.getElementsByClassName(elementClass);
onClass = cfg.onClass || '-ON';
elementTo = cfg.elementTo || 'div';
offsetViewportBy = cfg.offsetViewportBy === 0 ? 0.0001 : cfg.offsetViewportBy; // maybe use == truthy?
offsetViewportBy = offsetViewportBy || 1.5;
idleAttribute = cfg.idleAttribute || 'idle';
throttleScrolling = throttle(function() {
scrolling();
}, cfg.pollDelay, this);
callback = cfg.callbackFunc || false; // function name or falsey
// Ready, now let's roll
window.addEventListener('scroll', throttleScrolling, false);
// Run once to display anything already inside the viewport
scrolling();
}
}
Finally, expose the initialise function and end the closure.
return {init : initialise};
}());
The idleLoad.init() call sets off the whole thing, optionally passing in the configuration values.
It's possible to fire each <noscript> individually by adding a data-idle='1' which acts as an override for the value of offsetViewportBy.
For example the blathered Ewok which should appear below / to the right.
Special FX
No CGI here Mr Lucas, just a rough demo of what else may be achieved with idle-load.
Run a callback function once the content is added. For example the function loaded() :
idleLoad.init({
callbackFunction : loaded
});
The function adds an event listener for the image load event:
function loaded(obj) {
var img = obj.getElementsByTagName('img');
function bringIn () {
this.classList.add('ON');
this.parentNode.setAttribute('style', 'max-height:' + (this.height + 150) + 'px');
}
if (img) {
// wait until the image has fully loaded
img[0].addEventListener('load', bringIn, false);
}
}
Once the image has downloaded the container has it's max-height adjusted to accommodate the image height, and a class of "ON" is applied to enable the CSS filters.
Use the Force young Luke
, well okay it's CSS:
.idle-load-ON {
max-height:0;
overflow:hidden;
transition: max-height .5s ease-out;
}
.idle-load-ON img {
-webkit-filter:blur(12px) grayscale(50%) sepia(20%) opacity(0%);
filter:blur(12px) grayscale(50%) sepia(20%) opacity(0%);
transition: filter .5s ease-out .25s, -webkit-filter .5s ease-out .25s;
}
.idle-load-ON img.ON {
-webkit-filter:blur(0) grayscale(50%) sepia(20%) opacity(100%);
filter:blur(0) grayscale(50%) sepia(20%) opacity(100%);
}
Help the aged: IE 6, 7 & 8
Unfortunately the method cannot work in IE 7 & 8, though surprisingly it does in IE 6. If fallback support is required the only known workaround is to use conditional (spit) comments:
<div>
<!--[if (gt IE 8) | (!IE)]><!-->
<noscript class="idle-load">
<!--<![endif]-->
<img src="i/obi_wan_kenobi.jpg" alt="">
<!--[if (gt IE 8) | (!IE)]><!-->
</noscript>
<!--<![endif]-->
</div>
The JavaScript tests for feature support which are unavailable in IE less than version 9.
Social links and email client: