This is how BBC News currently implements it's Image Enhancer for responsive images. Note: this is a completely rebuilt version of the code so the BBC's original source code doesn't actually look anything like the below example.
<!doctype html>
<html dir="ltr" lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>ImageEnhancer</title>
<style>
img {
max-width: 100%;
}
a {
display: block;
margin-top: 1em;
}
</style>
</head>
<body>
<p>Here is our main image, we load this regardless of JavaScript support.</p>
<img src="http://placehold.it/340">
<p>Below are three <code>div</code> elements that will lazy-load more images (as long as JavaScript is enabled).</p>
<div class="delayed-image-load" data-src="http://placehold.it/340" data-width="340"></div>
<div class="delayed-image-load" data-src="http://placehold.it/340" data-width="340"></div>
<div class="delayed-image-load" data-src="http://placehold.it/340" data-width="340"></div>
<script>
var pubsub = (function(){
var doc = document;
var topics = {};
var id = -1;
var pubsub = {};
pubsub.subscribe = function (topic, fn) {
if (!topics[topic]) {
topics[topic] = [];
}
var token = (++id).toString();
topics[topic].push({
token: token,
fn: fn
});
return token;
};
pubsub.unsubscribe = function (token) {
for (var m in topics) {
if (topics[m]) {
for (var i = 0, j = topics[m].length; i < j; i++) {
if (topics[m][i].token === token) {
topics[m].splice(i, 1);
return token;
}
}
}
}
return false;
};
pubsub.publish = function (topic, data) {
if (!topics[topic]) {
return false;
}
setTimeout(function(){
var subscribers = topics[topic],
len = topics[topic].length;
while (len--) {
subscribers[len].fn(topic, data);
}
}, 0);
return true;
};
return pubsub;
}());
function ImageEnhancer(){
pubsub.subscribe('imageEnhancer:resize', this.resizeImages.bind(this));
pubsub.subscribe('div:added', this.changeDivsToEmptyImages.bind(this));
pubsub.subscribe('div:changed', this.resizeImages.bind(this));
this.availableWidthsFromOurImageProviderService = [96, 130, 165, 200, 235, 270, 304, 340, 375, 410, 445, 485, 520, 555, 590, 625, 660, 695, 736];
this.divs = document.getElementsByClassName('delayed-image-load'); // we use `getElementsByClassName` so we get a live NodeList
this.changeDivsToEmptyImages();
window.requestAnimationFrame(this.init.bind(this));
}
ImageEnhancer.prototype = {
changeDivsToEmptyImages: function(){
var i = this.divs.length;
while (i--) {
var div = this.divs[i],
img = document.createElement('img');
img.src = 'data:image/gif;base64,R0lGODlhEAAJAIAAAP///wAAACH5BAEAAAAALAAAAAAQAAkAAAIKhI+py+0Po5yUFQA7';
img.className = 'image-replace';
img.width = div.getAttribute('data-width');
img.setAttribute('data-src', div.getAttribute('data-src'));
div.parentNode.replaceChild(img, div);
}
if (this.initialised) {
pubsub.publish('div:changed');
}
},
init: function(){
this.initialised = true;
this.resizeImages();
window.addEventListener('resize', function() {
pubsub.publish('imageEnhancer:resize');
}, false);
},
resizeImages: function(){
var imageList = Array.prototype.slice.call(document.querySelectorAll('.image-replace'));
if (!this.isResizing) {
this.isResizing = true;
imageList.forEach(function (img) {
img.src = this.calculateNewImageSrc(img);
}.bind(this));
this.isResizing = false;
}
},
calculateNewImageSrc: function (img) {
var imageSrc = img.getAttribute('data-src'),
imageWidth = img.clientWidth,
selectedWidth = this.availableWidthsFromOurImageProviderService[0];
this.availableWidthsFromOurImageProviderService.forEach(function (currentlyAvailableWidth, index) {
if (imageWidth > currentlyAvailableWidth) {
selectedWidth = this.availableWidthsFromOurImageProviderService[index + 1];
}
}.bind(this));
return imageSrc.replace(/^(.+\/)\d+$/i, function (match, captured) {
return captured + selectedWidth;
});
}
};
function createAnchor(){
var anchor = document.createElement('a');
anchor.href = '#my_new_element';
anchor.innerHTML = 'Click me to add a new image to the DOM after ImageEnhancer has already been instantiated'
document.body.appendChild(anchor);
anchor.onclick = createNewImage;
}
function createNewImage(){
var div = document.createElement('div');
div.className = 'delayed-image-load';
div.setAttribute('name', 'my_new_element');
div.setAttribute('data-src', 'http://placehold.it/340');
div.setAttribute('data-width', '340');
document.body.appendChild(div);
pubsub.publish('div:added');
}
createAnchor();
new ImageEnhancer();
</script>
</body>
</html>
The BBC has a server-side image service which provides developers with multiple sized versions of any image they request. It works in a similar fashion to http://placehold.it/ but it also handles the image ratios returned (where as placehold.it doesn't).
The original BBC News process (and my re-working of the script) follows roughly these steps...
div
s within the page (which have a class of delayed-image-load
) into a transparent GIF using a Base64 encoded string.
width
& height
HTML attributes of the image to the required sizediv
has custom data-attr
set server-side to the size of the imageimage-replace
onto each newly created transparent imagesetTimeout
to unblock the UI thread and which calls a function resizeImages
which enhances the image-replace
images so their source is now set to a URL where the returned image matches the requested dimensions in the URL path (in my example below I use requestAnimationFrame
instead)
imageEnhancer:resize
resizeImages
initially and then every time the pubsub event fires.
image-replace
image and changes the src
attribute to the new URL with the best possible match dimensions specified within the URL path (based on the dimensions required for the image).The example code that follows is not production ready, you can't just drop it into your code base and expect it to work. It is a rough prototype I built in about 30 minutes to help demonstrate in a roundabout way how the BBC News responsive team were handling the enhancement of images. The code uses features only available to more modern browsers (e.g. I've tested it in Chrome 28 and nothing else so your mile may vary if you're using a different browser to look at the example).
I've also used the http://placehold.it/ service in place of the actual BBC News image provider. They don't work in quite the same way, but if you open the following HTML code in a modern browser (e.g. Chrome) and resize the browser window downwards you'll see the images change based on the current window dimensions. So rather than load a large image for smaller screens we swap out our images with images that are optimised for that screen size.
Notes about the rewritten example:
nodeList
otherwise we'll miss any images added to the DOM after ImageEnhancer is initialised.