Skip to content

Instantly share code, notes, and snippets.

@Integralist
Last active April 25, 2020 16:20
Show Gist options
  • Save Integralist/6157139 to your computer and use it in GitHub Desktop.
Save Integralist/6157139 to your computer and use it in GitHub Desktop.
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.

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...

  • Create new instance of ImageEnhancer
  • Change any divs within the page (which have a class of delayed-image-load) into a transparent GIF using a Base64 encoded string.
    • We set the width & height HTML attributes of the image to the required size
    • We know what size the image needs to be because each div has custom data-attr set server-side to the size of the image
    • We then set a class of image-replace onto each newly created transparent image
  • We use a 250ms setTimeout 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)
    • We also create an event listener for the resize event which fires a pubsub event of imageEnhancer:resize
  • We call resizeImages initially and then every time the pubsub event fires.
    • This function loops each 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).
    • If the resize event fires then we check the dimensions of the page and determine if we need a new URL to be used.

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:

  • I should use live nodeList otherwise we'll miss any images added to the DOM after ImageEnhancer is initialised.
  • I might be able to work-around the use of the base64 image
  • Look at caching of image loads
<!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>
@Integralist
Copy link
Author

@addyosmani that is correct yes, it is much more cross browser friendly and utilises less modern API's. but for the sake of simplicity in demonstrating the principles of the code I avoided worrying about cross browser support while carrying out this quick rewrite :-)

@AndreLion
Copy link

Where is BBC responsive server-side image service? I didn't find it on http://m.bbc.co.uk/news

@Maksims
Copy link

Maksims commented Aug 6, 2013

We've went very similar approach with our project (LoveWall) but we've experienced rare issues with streamed images when replacing src directly to image. The problem was that if replace happens few times, after last image loaded it would throw http status code as (Failed) for some weird reason, and then would render image as corrupted.
In order to prevent that, each image has own Image js object, and if we need to load another image before this one loaded - we just remove it, that prevents http request to that image, and switches to new one. Then once it is loaded (onload) we just attach src to our actual element, and it appears immediately (as already in cache).

@robwierzbowski
Copy link

Thanks for putting this up!

Curious what advantages you see in your code over Picturefill. And I might have missed it, but it seems like there isn't a no-js fallback for the responsive image divs. Can you give some insight into that decision?

@Integralist
Copy link
Author

Sorry to anyone who posted a comment and I didn't reply, annoyingly GitHub doesn't alert an author of a gist if comments are made (really not sure why).

For more information read: http://responsivenews.co.uk/post/50092458307/images followed by http://responsivenews.co.uk/post/58244240772/imager-js

This gist was created as a favour to Addy Osmani (Google Dev Relations Engineer). We have (since this gist was created and those two blog posts linked above) gone on to open-source our responsive image solution under the name Imager.js

You can find Imager.js here: https://github.com/BBC-News/Imager.js/#imagerjs-

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment