Simple Image Lazy Load and Fade

By  on  

One of the quickest and easiest website performance optimizations is decreasing image loading.  That means a variety of things, including minifying images with tools like ImageOptim and TinyPNG, using data URIs and sprites, and lazy loading images.  It's a bit jarring when you're lazy loading images and they just appear out of nowhere which is why I love the fading in route.  The page still shuffles if you aren't explicitly setting image dimensions but the fade in does provide a tiny bit of class.  I've seen many solutions which accomplish this (some not very good, like my old method) so I thought I'd share my current implementation.

The HTML

We'll start by putting together the image tag with specifics:

<img data-src="/path/to/image.jpg" alt="">

Use data-src to represent the eventual URL.

The CSS

Any image with a data-src attribute should start as invisible and eventually transition the opacity:

img {
	opacity: 1;
	transition: opacity 0.3s;
}

img[data-src] {
	opacity: 0;
}

You can probably guess at this point what we'll be doing with that attribute when an image loads...

The JavaScript

...which is removing the data-src attribute when the image has loaded:

[].forEach.call(document.querySelectorAll('img[data-src]'), function(img) {
	img.setAttribute('src', img.getAttribute('data-src'));
	img.onload = function() {
		img.removeAttribute('data-src');
	};
});

This solution does require JavaScript as a few of you have pointed out. For a fallback solution you could do this:

<noscript data-src="/path/to/image.jpg">
<img src="/path/to/image.jpg" data-src="" alt="">
</noscript>
[].forEach.call(document.querySelectorAll('noscript'), function(noscript) {
	var img = new Image();
	img.setAttribute('data-src', '');
	img.parentNode.insertBefore(img, noscript);
	img.onload = function() {
		img.removeAttribute('data-src');
	};
	img.src = noscript.getAttribute('data-src');
});

This is a super basic tutorial but considering I've seen so many other solutions, I thought I'd share what I've implemented;  it works under every scenario I've tested, including History changes via AJAX (like my site does).

Of course this doesn't take into account true scroll-based lazy load but that's generally done by a plugin in your favorite JavaScript framework or a standalone component. If you're looking for a simple solution, however, this is it!

Recent Features

  • By
    Serving Fonts from CDN

    For maximum performance, we all know we must put our assets on CDN (another domain).  Along with those assets are custom web fonts.  Unfortunately custom web fonts via CDN (or any cross-domain font request) don't work in Firefox or Internet Explorer (correctly so, by spec) though...

  • By
    fetch API

    One of the worst kept secrets about AJAX on the web is that the underlying API for it, XMLHttpRequest, wasn't really made for what we've been using it for.  We've done well to create elegant APIs around XHR but we know we can do better.  Our effort to...

Incredible Demos

  • By
    Duplicate the jQuery Homepage Tooltips Using Dojo

    The jQuery homepage has a pretty suave tooltip-like effect as seen below: Here's how to accomplish this same effect using Dojo. The XHTML The above HTML was taken directly from the jQuery homepage -- no changes. The CSS The above CSS has been slightly modified to match the CSS rules already...

  • By
    Fullscreen API

    As we move toward more true web applications, our JavaScript APIs are doing their best to keep up.  One very simple but useful new JavaScript API is the Fullscreen API.  The Fullscreen API provides a programmatic way to request fullscreen display from the user, and exit...

Discussion

  1. jacky

    How about replacing

    img.setAttribute('src', img.getAttribute('data-src'));
    

    with

    img.setAttribute('src', img.dataset.src);
    

    ?

    • trhgrefgrege

      Doesn’t work in IE9

    • eeee

      Actualy, dataset prop is slower than get/setAttribute methods

    • Valtteri

      Or

      img.src = img.dataset.src;
      
    • Valtteri

      And to remove, use:

      delete img.dataset.src;
  2. Tune

    This is very nice, but is there a way to implement it with a no-js fallback? I would prefer a solution where the HTML code has the image urls in the src attribute so that search bots/crawlers can grab the (semantically) correct HTML and that Javascript starts moving around src values before the images start being loaded to provide the lazy loading.

    • Updated!

    • SEO/crawlers is one thing I’ve always worried about with this method too. Does adding the tag with an img and src element definitely allow the image to be indexed?

      Cheers

    • Conditions like category listing in a shopping cart, images aren’t really that important to the search engine … It can extract the data from the full details.

      Or at least I think so

    • It seems as if the proposed fallback solution does not work as expected. While it is possible to query all noscript elements in most browsers I tested they do not have child nodes. As a consequence it is not possible to query images in noscript tags using document.querySelectorAll.

      It seems as if I am not the only one having those problems: http://stackoverflow.com/questions/620896/access-contents-of-noscript-with-javascript

    • After some testing I came up with a better solution! Updated the post!

    • According to https://developer.mozilla.org/en-US/docs/Web/API/Node/insertBefore the Syntax for insertBefore is a bit more complicated. It should be: img.parentNode.insertBefore(img, noscript) instead of img.insertBefore(noscript)

    • Derp, sorry, thinking too slowly this morning.

  3. Matías Pizarro

    I wrote a little jQuery plugin for lazy load images when user scroll into it.

    Here is on Github: https://github.com/MaTyRocK/preimageload

  4. Didn’t anyone ever tell you not to use forEach like that?!

    http://toddmotto.com/ditch-the-array-foreach-call-nodelist-hack/

    An ordinary for loop would do ;-)

  5. Thanks for this, I decided to make it into a react component! http://github.com/legitcode/image

  6. I like this method a lot. I think combining this with low quality image placeholders (to handle the jumpiness from no image dimensions) could be very powerful.

    One thing I noticed is that with the CSS you posted, the noscript image has a empty data-src so it will be invisible due to img[data-src] { opacity: 0; }.

    • If you want to create placeholders you may find the concepts in my article Dominant Colors for Lazy-Loading Images interesting. In there I outline how to extract the dominant color from images and how to create small placeholders as Data URI to save the requests.

      Hope you don’t me, posting this, David. Great and clear write-up of a very concise method!

  7. Chris

    I tried to get this to work, but I seem to keep getting parentNode of img is null in the fallback code. After some research it seems that there are no childnodes inside a tag in Chrome if JS is running?

    So I resorted to just wrapping my image inside and went with the first, no fallback approach. Am I doing something wrong?

    • Not sure until we see the code :)

    • To test, I literally copy and pasted the fallback html into my index.html, and the js loads before on document ready. No craziness going on. In this jsfiddle, it produces the same result. At this point, I’m leaning towards this being user error and not picking up on my own stupid mistake :)

      https://jsfiddle.net/chris_s/h27u9r4j/

    • Sure, the img isn’t yet placed into the DOM, you need to use the noscript element to get the parentNode
      https://jsfiddle.net/uda/gabrmoxy/

    • (My previous comment was with the wrong link)
      You need to get the parentNode from the noscript element instead of the img which hasn’t yet been placed into the DOM tree.
      https://jsfiddle.net/uda/gabrmoxy/1/

  8. Nico

    Hi David, Thanks for this useful post !
    I’m trying to copy original image attributes but can’t get it working … Any ideas ? Here’s my code :

    [].forEach.call(document.querySelectorAll('noscript'), function(noscript) {
        var originalImage = noscript.children;
        var img = new Image();
        for (var i = 0; i < originialImage.attributes.length; i++) {
            var attr = originialImage.attributes.item(i);
            img.setAttribute(attr.nodeName, attr.nodeValue);
        }
        img.setAttribute('data-src', '');
        noscript.parentNode.insertBefore(img, noscript);
        img.onload = function() {
            img.removeAttribute('data-src');
        };
        img.src = noscript.getAttribute('data-src');
        img.src = img.dataset.src;
    });
    
  9. Is there any reason to use the data-src attribute instead of adding/removing a class like visible? Are there performance benefits? Or is it just maybe more descriptive to use data-src?

    • Rae

      Hi, David, Thanks for this. Very straightforward. Have it working where first image on the page fades up nicely, but as I scroll the other images are already loaded. Am looking for the visual effect of the image fading up as you scroll. Have slowed the transition, but still doesn’t affect the subsequent images. Any thoughts?

Wrap your code in <pre class="{language}"></pre> tags, link to a GitHub gist, JSFiddle fiddle, or CodePen pen to embed!