Defer Images with No Script Fall-back?

I have this site on which I created a php functions and sql tables that stores and renders css animated gallery slideshows. The tables store the gallery info, pictures, duration etc, and php renders the html and css to make it go. So no js involved so far (I do tend to keep js to a minimum).
What I find is, loading all these images is a bit of a drag on the homepage loading, so thought it a good idea to defer the loading.
I did a search and found a nice short, simple script to do that without the need for Jquery or any of that kind of stuff.

It does seem to work, but the problem is, this makes my “CSS Only” slideshow totally reliant on js :upside_down: as the src attributes all contain a placeholder until the script replaces them.
Ideally I would like this deferral to be an “enhancement” for those with js working, so without it, you would still see the slideshow, albeit on a possibly sluggish page.
I guess that would need to src to initially be the proper image url, but then a script remove that until loading, then replace it. Thought that seems like a nonsensical paradox, to have js swap the url, then swap it back. Won’t it try load the images before the script defers them?
Or would you put the “normal” <img> in a noscript block, and have js render the deferred ones, or does that seem a bit of a hack?
I guess the question is: how do those who still care for progressive enhancement handle image deferral?
This is the homepage: winter ton show.co.uk with the slideshow. At time of posting, it does not have the deferral script, I just tested it locally so far.

I don’t know if this approach could fit into your scheme of things as it is a bit specific but you could add a class of no-js to the html element and use js to remove that class if it is activated.

Then you could use css to supply a background image to the image.

e.g.

<!DOCTYPE HTML>
<html class="no-js">
<script>
document.documentElement.className = document.documentElement.className.replace(/\bno-js\b/g, '') + ' js ';
</script>
<head>
<meta charset="utf-8">
<title>Untitled Document</title>
<style>
.no-js .img1{
	width:0;
	height:0;
	padding:722px 0 0 1000px;/* width and heigh of image*/
	background:url(images/a1.jpg) no-repeat 0 0;
}
</style>
</head>
<body>
<div class="test">
<img class="img1" src="fake.jpg" data-src="images/a1.jpg" width="1000" height="722" alt="test"></div>
</body>
</html>

In the above the fake.jpg gets rendered unless you remove the js and then the original image is shown as a background to the image.

It does mean that the css padding has to match image widths and heights and the background needs a url but I guess you could do that serverside somehow if images are dynamic and variable.

It may be too awkward though so was was just an idea :slight_smile:

1 Like

Not sure I fully get it (that could be down to man-flu) I may have a proper look at it some time though.

The back-end is already a bit crazy, this started out as a experiment to take the donkey work out of hand coding the css for such a gallery and it worked, so made to the live site. It just turned out to be a page-load hog, hence the need for deferrals.
Just for now I have made quick-fix which is a bit of a compromise, but I think an improvement.
As it was, with js off, you just got a blank black frame, which is the black background colour of the <ul>.
With a tweak to the code that renders the css I made it place the first image of the sequence as the background image. So now with js off you just get a single still image instead of a blank frame. Not ideal, but OK.

That would have been my take… the the src properties should get removed in time if you just put the JS at the end of the body as usual; alternatively, there’s actually an event for this:

const images = document.getElementsByClassName('deferred')

document.addEventListener('DOMContentLoaded', () => {
  Array.from(images).forEach(img => {
    img.dataset.src = img.src
    img.src = null
  })
})

You can put this bit in the head and the rest of your JS at the bottom of the body then; images would be a live list of the deferred images for better performance. Yeah maybe somewhat paradoxical, but OTOH it doesn’t require any additional markup or CSS… just the JS.

2 Likes

All it really does is it hides the foreground image by setting the images width and height to zero. This means that the image has nothing in which it can be seen. Then we duplicate the image size by using padding and apply a background image to the image instead. People don’t realise that you can add a background image to an image although most of the time you wouldn’t see it because the image foreground would hide it (unless the image was transparent).

If you use a transparent gif or (data uri) as the default image and assuming the image always has height and width attributes then you can simplify the CSS above and only need to send the image url for each image in the css.

e.g.

<!DOCTYPE HTML>
<html class="no-js">
<script>
document.documentElement.className = document.documentElement.className.replace(/\bno-js\b/g, '') + ' js ';
</script>
<head>
<meta charset="utf-8">
<title>Untitled Document</title>
<style>
img{display:block;margin:20px auto;/* just for testing*/}
.no-js .img1{
	background:url(http://www.pmob.co.uk/temp2/images/a1.jpg) no-repeat 0 0;
	background-size:cover;/* just in case*/
}
.no-js .img2{
	background:url(http://www.pmob.co.uk/temp2/images/seascape2.jpg) no-repeat 0 0;
	background-size:cover;/* just in case*/
}

</style>
</head>
<body>
<img class="img1" src="data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7" data-src="http://www.pmob.co.uk/temp2/images/a1.jpg" width="1000" height="722" alt="Car">
<img class="img2" src="data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7" data-src="http://www.pmob.co.uk/temp2/images/seascape2.jpg"  width="500" height="500"  alt="Seascape">
<script>
(function() {
	var imgEl = document.getElementsByTagName('img');
    for (var i = 0; i < imgEl.length; i++) {
        if (imgEl[i].getAttribute('data-src')) {
            imgEl[i].setAttribute('src', imgEl[i].getAttribute('data-src'));
        }
    }
}());
</script>

</body>
</html>

With or without js the image is still there. Not sure it will help with your problem at the moment as you would still need to write the css to match the html img src.

In the future (hopefully) you should be able to use the data attribute to supply the url for the background image and thus automate the process entirely.

@m3g4p0p 's solution looks neater though :slight_smile:

Thanks guys. For now I have added the deferral script to the site with just the single background as a fall-back which is better than none. But I will look onto this next week when I have the time. I will probably try @m3g4p0p s example as it appears to be what I was looking for. I may set up a new gallery with more and higher-res images for testing purposes.
If all goes smoothly it’s something I may want to use elsewhere for below the fold images.

Actually, it just occurred to me that the image loading might get started and immediately cancelled. Oops! :-$ Still saves the bandwidth, but this is of course not ideal… so here’s the noscript version:

<noscript class="deferred-container">
  <img src="http://lorempixel.com/400/200/city/1/">
  <img src="http://lorempixel.com/400/200/city/2/">
</noscript>
const containers = document.getElementsByClassName('deferred-container')
const parser = new DOMParser()

Array.from(containers).forEach(container => {
  const images = parser
    .parseFromString(container.textContent, 'text/html')
    .getElementsByTagName('img')

  const fragment = Array
    .from(images)
    .reduce((fragment, image) => {
      image.dataset.src = image.src
      image.src = ''
      fragment.appendChild(image)

      return fragment
    }, document.createDocumentFragment())
  
  container.parentNode.replaceChild(fragment, container)
})

That would certainly be the safer route (as is of course @PaulOB’s solution)… this time I kept a closer eye on the network panel. ^^

If it is a new project try using AMP.

Using `<amp_image* takes care of the JavaScript and above the fold problems and resizes images if using responsive to the containing element.

I got around to revisiting this after getting distracted by more pressing things.
I think I have it working, using the last bit of script from @m3g4p0p and the original bit of defer script, both at the end of the body. Though being hopeless with js, I’m not entirely sure how or why it works, or that that is the intended/correct set up.
What I think is happening is, the above script is removing the noscript tags from the images, and removing the src. Then the original script is re-populating the src from the data.
This is a mock-up page for testing: http://winter ton show.co.uk/gal test001.html it has two slideshows on it.
When I try it on Pingdom, it does not show the images images loading in the request list. I think that means it works.
With the original defer setup, it did show them load, but they were last in the list.

Yes, that would be the set up… having had a look at your mock page though, I think that whole data-src step is not even necessary (any more). Since there’s no lazy-loading involved and the src’s are immediately getting re-set anyway, you might perhaps just replace the noscript elements with their img children on load like

window.addEventListener('load', function replaceNoscript () {
  var containers = document.getElementsByClassName('deferred-container')
  var parser = new DOMParser()

  Array
    .prototype
    .slice
    .call(containers)
    .forEach(function (container) {
      var images = parser
        .parseFromString(container.textContent, 'text/html')
        .getElementsByTagName('img')

      var fragment = Array
        .prototype
        .slice
        .call(images)
        .reduce(function (fragment, image) {
          fragment.appendChild(image)
          return fragment
        }, document.createDocumentFragment())

      container.parentNode.replaceChild(fragment, container)
    })  
})

This would basically have the same effect then. If you want to defer the image loading to a point when they are supposed to be visible (e.g. when sliding the slide show), those data-* attributes would certainly be the way to go though.

1 Like

OK. I think this is working now with the new script.
The slideshow plays with js both on or off.
When I check the waterfall in gtmetrix, all the slideshow images get loaded after “onload” which suggests the deferral is working as intended.
The real “news” page is now weighing in at just under 2MB, which isn’t too bad for a page with two slideshows, at least when you don’t need to wait for the bulk of it to start reading.
Something to remember for any heavy, below the fold content.
Thank you. :smiley:

1 Like

Cool, glad I could help! :-) Just a small addendum since I’m so used to writing ES2015 now… I’ve edited my last post for IE compatibility. ^^

With this version, I only see every even image.
In inspect the <noscript> tags remain on the even images. :confused:

Boing. Fixed that, thanks!

1 Like

That seems to be working now. Thanks again.
Not sure why the animation doesn’t play in IE11. But that’s another story, must be a CSS thing not JS.

1 Like

This topic was automatically closed 91 days after the last reply. New replies are no longer allowed.