When building responsive websites, you immediately run into the problem of trying to implement responsive images which would be sized correctly depending on the user's device. Either you want to hide images from mobile users or give them an optimised version. The problem centers on the page loading anything referenced in the HTML even if it’s hidden by CSS in your media queries. This can lead to your mobile users getting a big hit from content they aren’t even using.
This problem can’t be handled entirely on the server side. You have to make contact with the user’s device before you can determine how much screen width is available. Therefore the solution has to involve JavaScript. The feature detection has to intercept the server request before the entire DOM has loaded into place, otherwise the page could make a request for the wrong image. You also have to be able to serve an image to users who have JavaScript switched off, so a full JavaScript solution is not acceptable.
We came across a couple of existing libraries. You can read our blog post on implementing the Filament Group solution. We later found another solution, which suggests using PHP to serve your images. Browser support was an issue for us with the Filament Group solution so we started experimenting with our own ideas. We came up with the idea of adding an image into a <noscript> tag and then enhancing the site for JavaScript users by removing the <noscript> tag and creating a new image element with a dynamic source attribute.
Using the Webkit developer tools, HTTPWatch for Internet explorer and HTTPfox for Firefox we were surprised to discover that removing the element with JavaScript prevented the request being made to the server. After a quick Google we discovered that it works because children of the <noscript> tag are not added to the DOM. This appears to work in all the browsers we’ve tested, even IE6, with the single exception of Opera’s Dragonfly, which crashed repeatedly when we tried to test.
We created a Rails helper gem, responsive_image_tag, to insert the necessary markup into the page:
1 2 3 4 |
<span class="img-placeholder"></span> <noscript data-mobilesrc="small.jpg" data-fullsrc="big.jpg" data-alttext="your alt text" class="responsivize"> <img src="big.jpg"> </noscript> |
The helper places the default image inside a <noscript> tag, which is then deleted by the JavaScript library. The image attributes such as full size, mobile size and alt text are also stored as HTML5 data attributes on the <noscript> tag so they are available in the DOM for the JavaScript to access.
The library relies on the premise that child elements of the <noscript> tag are not added to the DOM, so deleting the <noscript> prevents an HTTP request being made to the server. This way only the image being requested by the dynamically inserted image tag is making a request.
To insert the dynamically created image element into the page you need a parent element in the DOM to append to. The Rails helper also creates a <span> tag with a class of “img-placeholder” to house the new image.
When the DOM is ready the JavaScript object responsiveImageTag detects all <noscript> elements on the page with a class of “responsivize”.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
var responsiveImageTag = { replaceInitialImages:function() { var platform = "desktop"; var responsiveImages = $$(".responsivize"); var i, noOfresponsiveImages = responsiveImages.length; //test for available width in current browser window if(screen.width <= 767){ //767px, anything smaller than an ipad is considered mobile platform = "mobile"; } //set initial source element on image for(i = 0; i < noOfresponsiveImages; i = i + 1 ){ var noScriptElem = $(responsiveImages[i]); |
It retrieves the HTML5 data attributes ("data-mobilesrc", "data-fullsrc", and "data-alttext") and then creates an image tag with the correct source depending on the available screen width of the users device.
1 2 3 4 5 6 7 8 |
var img = window.document.createElement("img"); img.alt = noScriptElem.attr("data-alttext"); if(platform === "mobile"){ img.src = noScriptElem.attr("data-mobilesrc"); }else{ img.src = noScriptElem.attr("data-fullsrc"); } img.className = "responsive"; |
The responsive-image-tag javascript inserts the new element into the page and the <noscript> tags are then hidden from view.
1 2 3 4 5 |
noScriptElem.prev().append(img);
noScriptElem.hide();
}
}
}; |
You can check out our github repo. It’s not a perfect solution. It has the elegance of a wading hippo. It adds a lot of extra markup into your page for each image tag you want to make responsive and it’s causing a page reflow as your page is loading, which could potentially impact performance if over-used.
Ideally we need an addition to the HTML5 specification to allow some kind of device recognition or feature detection for device specific assets. I believe there are several aspects of developing for mobile or developing responsively that we don’t have real standard support for right now and as a development community we need to be championing the need for these and getting them written into the spec now.
12 comments
Morgan Christiansson says:
on Thu 18 August, 2011
Did you consider using CSS to apply the images? As described on http://developer.android.com/reference/android/webkit/WebView.html you can then use CSS media queries to target specific resolutions: <link href="hdpi.css" /> This keeps the design information out of the HTML. It limits you tu using CSS backgrounds for all images but it does keep the design and logic out of the HTML.
mairead says:
on Thu 18 August, 2011
I think for accessibility purposes putting your content images into the CSS is a bad idea. You need to have them in the page with the right alt text associated so the content is available to a screen reader. I think from the point of view of separating your concerns you want to keep CSS images purely decorative in the style layer and your semantic content in the HTML
john says:
on Mon 29 August, 2011
wake up !!!! in the end HTML/CSS/JS will be a language that will eventually only be understood by machines.. It will eventually become machine language. We need to start thinking at a higher level!! Look at the majority of the worlds biggest sights, how much of those are minified JS and how much of there HTML/JS is readable.. It will only get worse over time!!! get over yourselves and stop trying to spec the crap out of everything!
Jeremy Keith says:
on Mon 29 August, 2011
Hi Mairead, Well done on getting this working! I had a similar idea recently but when I tried to implement it, I couldn't get access to the noscript elements: document.getElementsByTagName('noscript') will always return an empty set (which makes sense I guess, considering that JavaScript shouldn't be able to "see" the noscript stuff). I wonder if it's working for you because a) you're accessing by class name rather than element name or b) because you're using jQuery? Intriguing! In any case; nice work.
bartaz says:
on Tue 30 August, 2011
Wouldn't it make more sense to put small (mobile) image into noscript and replace it with big with JavaScript? Most mobile devices don't support JavaScript or don't run it very well (you can forget about jQuery or Prototype there), so why to force them to download big images?
mairead says:
on Tue 30 August, 2011
Hey Jeremy, We wanted to put our data attributes on the image tag inside really but I think only mozilla allows you to access the content inside the noscript so we ended up putting them there instead. We built a version in jquery and prototype, because our site is a rails 2 site and prototype was required. It worked either way, my understanding was its only the DOM structure inside the noscript which is inaccessible. Bartaz - you could put either version inside the noscript it doesn't really matter to be honest, I guess it depends on whether you think your desktop users are more likely to have javascript off than your mobile users. Although I would say most modern smartphones are running javascript, certainly the iphone/android market we are targeting. Apple were struggling a bit with their mobile javascript engine but they seem to have pulled their socks up a bit I think as long as you've got your javascript optimised enough then using a library is perfectly acceptable but again that depends on your audience.
Scott Hulbert says:
on Tue 30 August, 2011
What's the impact of all this content not showing in the DOM to things like screenreaders or spiders?
Dave Hrycyszyn says:
on Sat 10 September, 2011
@Scott Hulbert: Having done a bit more research into *why* this thing works, screenreaders and spiders should be fine with this approach. "script" and "noscript" tags essentially act as "if-then" statements within the context of the browser. So, if the user doesn't have javascript enabled, the contents of the "noscript" tag will be inserted into the dom (and the image request takes place normally). In this case, screenreaders and spiders would see what they normally see - the contents of the noscript tag. In the case where the javascript is enabled, the noscript content is never executed by the browser, which is why no double image-load takes place.
Matt Wiebe says:
on Wed 21 September, 2011
Loving this approach. I agree with bartaz that it probably makes sense to put the lowsrc inside the noscript, but otherwise this is great. One note: at least with jQuery, I'm finding that I'm able to use the .before() method to simply append the img before the noscript tag rather than needing to have an extra span element there. Working on a WordPress plugin for this approach.
Josh says:
on Wed 25 January, 2012
Effective solution! One suggestion, in legacy IE (7 & 8) the img element is added with the width and height attributes. For use with responsive images that are being scaled in the browser, either remove the attributes in the javascript or be sure to style the img with "auto" width and height in the CSS.
Sam says:
on Wed 08 February, 2012
Top stuff. Really like this approach - it's pragmatic, and as for the waddling hippo comparison, fear not. This is the web, and best practice solutions are often of that variety!
david clements says:
on Fri 20 April, 2012
what if instead if wrapping each image with noscript, you wrap the whole body with it, then pull all of the content out of the noscript tags, modifying the all the img srcs accordingly and placing it dynamically into the body - you couldn't modify img src's with DOM manipulation but you could do it regex replace - alternatively you could use a dom parser written Javascript (like jsdom https://github.com/tmpvar/jsdom). This would reduce the html weight, but could maybe be too heavy on the processing?