Area 51

Sporadic links and brief articles on #design, #CSS, #iOS, #productivity and the things we have #shipped.

Handling Images in JavaScript

— 18 Min Read

When I started redesigning this site, I wanted a simple way to process images, covering a few basic needs. While there are various methods being formed for handling responsive images and display density, there is currently no prescribed approach. This site has no server code, so JavaScript was the obvious choice.

My requirements were:

  • swap in @2x or @3x versions for high-DPI displays, ideally, without downloading the original @1x
  • lazy-load some images in defined contexts
  • display a fallback colour while images are loading in the background to reduce perceived load time
  • embrace responsive design and keep markup simple

I wrote a function which passes one or more imgs through a switch and creates a new Image object for each, appending the desired attributes, before replacing the original with a new img.

I'll go through each stage of imageHandler() in detail before linking to the full source but, first, let's take a look at the markup. This is what an image looks like:

<img src="image.png">

To have that image lazy-load, do this instead:

<img src="{{ site.lazy }}" data-src="image.png">

site.lazy is a Liquid variable I have defined in Jekyll's config.yml, which outputs the smallest possible Base64-encoded .gif, preventing the image from returning a 404 before JavaScript runs:

data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7

If you are not using a site generator or CMS, you may have to manually enter this string each time.

While the image loads in the background, it is nice to display a fallback colour:

<img data-fallback="#f33">

Lastly, if @2x or @3x versions of the image are available, we can flag that too. I've called them retina and plus:

<img data-retina data-plus>

Any combination of these attributes and properties can be used. The end result might look something like this:

<img src="{{ site.lazy }}" data-src="image.png" data-retina data-plus data-fallback="#f33">

The imageHandler() function contains a switch with two cases; 'fallback'—called on the DOMContentLoaded—and 'parse', which is called on window.onload.

function imageHandler(action, image, callback) {
    switch (action) {
        case 'fallback':
            
        break;
        case 'parse':
            
        break;
    }
};

'fallback' is simple; it conditions the existence of data-fallback on an img and, if present, sets its value as the background-color:

var fallback = image.hasAttribute('data-fallback');

if (fallback) {
    image.style.backgroundColor = fallback;
}

There is a little more to case 'parse'. First, we prepare a new attributes object and a new image object:

var attrs = new Object();
var newImage = new Image();

Then, we take the original images' attributes and place them into the attrs object we just defined:

for (var i = 0, nodes = image.attributes; i < nodes.length; i++) {
    attrs[nodes[i].nodeName] = nodes[i].nodeValue;
}

Now we have everything we need for JavaScript to do its work. First, lazy-loading:

if ('data-src' in attrs) {
    attrs.src = attrs['data-src'];
}

This will replace the src value with the original data-src attribute we defined in markup.

Next, condition the display's pixel density and define a src for the new image:

var path = attrs.src.substring(0, attrs.src.lastIndexOf('.'));
var extension = attrs.src.split('.').pop();

if (window.devicePixelRatio > 2 && 'data-plus' in attrs) {
    newImage.src = path + '@3x.' + extension;
} else if (window.devicePixelRatio > 1 && 'data-retina' in attrs) {
    newImage.src = path + '@2x.' + extension;
} else {
    newImage.src = attrs.src;
}

Note: this assumes your images are named with the following convention:

  • image.png
  • image@2x.png
  • image@3x.png

As you can see, if window.devicePixelRatio is not greater than 1, we simply use the src value which may or may not have been lazy-loaded.

After that, it is important to remove the attributes that are no longer needed so that anything else (such as the alt attribute) can be preserved and restored:

var garbage = ['src', 'data-src', 'data-retina', 'data-plus', 'data-fallback', 'style'];

for (var i = 0; i < garbage.length; i++) {
    delete attrs[garbage[i]];
}

for (var attr in attrs) {
    newImage.setAttribute(attr, attrs[attr]);
}

The new Image is now ready. Once it has loaded, we can swap it in for the original:

newImage.onload = function() {
    image.parentNode.insertBefore(newImage, image);
    image.parentNode.removeChild(image);

    if (callback) {
        callback();
    }
};

The reason for going through the process of building a new image rather than simply manipulating the attributes of the original is that, with this approach, it is possible to fire a callback once the image loads. For instance, you may want to display some form of activity-indication while the image is loading and remove it onload.

Lastly, we need to call the function on the relevant events:

document.addEventListener('DOMContentLoaded', function() {

    forEach(document.querySelectorAll('img[data-fallback]'), function(index, image) {
        imageHandler('fallback', image);
    });

}, false);

window.addEventListener('load', function() {

    forEach(document.querySelectorAll('img'), function(index, image) {
        imageHandler('parse', image);
    });

}, false);

Note: in this example I am using a custom forEach method to avoid NodeList hacks. More on that here. Feel free to loop over imgs in whichever way you choose.

The full source of the imageHandler() function is available as a Gist here.

This is how every image on 51bits.com is handled. Some images lazy-load, others do not. Some have @3x support, others maybe only @2x. It is not perfect but as there is yet to be a silver bullet for handling images on the web, this fits my needs so I feel it is worth sharing. I'm no JavaScript master so there could be better ways to do this, but it works for me.

Known issues and room for improvement:

  • Stricter validation such as checking that the value of data-fallback is a string before setting it as a background-color
  • Since the images are handled by JavaScript, when the raw source is interpreted by something other than a browser (RSS, print, Safari Reader etc.) the lazy-loaded images will not render
  • During garbage collection, the style attribute is removed which may or may not be acceptable if your images have other inline styling

51bits 3.0

— 1 Min Read

Ten years ago, I registered a business name and decided I would work for myself. With the anniversary looming and the last major redesign of this website being in 2013, an evolution was in order. Today, I'm launching a new mark and website to complement the milestone.

51bits Marks 1-3
Marks 1–3

2.0 did away with a traditional visual portfolio in favour of showcasing products. The problem was that some projects take months or years to complete so parts of the site rarely received fresh content. Apps have now been sectioned off and there are plans to display work-in-progress with a future release.

Writing

I've tried on many occasions to find a way to write more often; sometimes succeeding for short periods of time before letting the frequency taper off. By sharing links supported by shorter, opinionated commentary as well as the occasional article, perhaps I can keep things moving at a steadier pace.

As I did with 2.0, a fair amount of content that I deem obsolete or even incorrect has been archived.

Going Vanilla

I forced myself to take the time and do things right. Every line of code is bespoke. There is no JavaScript framework, no server code and nothing that doesn't need to be here. As a result, I'm seeing significant performance improvements.

There will be many more updates from here. I've already logged over 30 bug and improvement tickets, but it's great to have a relatively fresh slate once again.

WillyWeather 2

— 1 Min Read

Last week, the cover was lifted from the largest project I've ever worked on. The new WillyWeather website is now live for Australian users.

WillyWeather 2
WillyWeather 2 is fully responsive

WillyWeather is a much-loved brand in Australia, with 15% of the population (and growing) frequenting it on iOS, Android or the Web every month. Redesigning a well-established brand and product is a daunting task; knowing that, no matter what, you will inevitably be on the receiving end of reactionary feedback from those adverse to change. Nonetheless, feedback has overall been very positive.

The new website is packed with excellent new features, including:

  • Fully responsive and high-DPI supported design
  • Three-hourly weather forecasts
  • Detailed and customisable forecast and observational graphs
  • Mappable forecast and observational imagery
  • Historical weather statistics
  • An overhauled Account System with paid features like SMS Notifications and Ad-Free browsing

We think it's the best weather site around and hope you do too!