logo

Mead.Design

Build a Lazy Image Loader in JS

September 14th, 2016

I have recently been having a lot of fun teaching an interaction design class, and had been wanting to play around with building a lazy image loader that displays a blurry low resolution very small image initially, then fades in a high resolution, larger, sharp image asynchronously, after that image has loaded. You can see a good implementation of this type of effect on medium.com.

My strategy is to simply have a small blurry image load, then once the full sized image has loaded add it to the DOM and just put it right on top of the small image. The fade in effect can be achieved with just a little CSS. Take a look at this simple demo to see it in action. Let’s take a look at the code. You can see in the HTML markup, only the small image is in the file…

<body>

<header id="loader">
	<img src="smallphotos/photo1.jpg" alt="museum">
</header>

Then, after the body tag I added this simple JavaScript…

<script>
	var image = new Image();
	image.src = "largephotos/photo1.jpg";
	
	image.onload = function () {
		
		console.log("Image loaded!");
		
		var theImage = document.createElement('img');
		theImage.src = "largephotos/photo1.jpg";
		theImage.className = "fullsized fade";
		document.getElementById('loader').appendChild(theImage);
	}
	
</script>

Line 2 creates a new image object and line 3 assigns the source. At this point, the browser will start downloading the source file from the server. The anonymous function on line 5 runs once the image has completely downloaded – however long that takes. This function then creates a new image element, assigns the source file to it (the one we just downloaded and is now in the cache). It then assigns two classes to the image and adds it to the header, which has an ID of loader.

Let’s take a quick look at the two classes that were added.

.fullsized {
	position: absolute;
	top:0;
	left: 0;
	width: 100%;
}

.fade {
	animation: fadein 2s ease-out forwards;
}

@keyframes fadein {
	from {
		opacity:0;
	}
	to {
		opacity:1;
	}
}

You can see that the fullsized class is absolutely positioning the image on top of the low resolution blurry image. The fade class fades it in, giving an effect of going from blurry to clear. Note that I have left all the browser prefixes off the fade and keyframes rules for simplicity.

It is interesting to point out, that in this case, the images are actually elements on the page. This technique works just as well if you want to load the images as background images in the CSS. Check out the second demo to see a version that loads the images as background elements in CSS.

<script>
	
	var image = new Image();

	image.src = "largephotos/photo1.jpg";

	image.onload = function() {

		console.log("Image loaded!");

		var theContainer = document.createElement('div');
		theContainer.className="loader fade";
		document.getElementById('lazyload').appendChild(theContainer);

	}
	
</script>

The difference here is that instead of having the anonymous function add an image element to the page, it adding a div. The loader class that is attached to the DIV uses the high resolution image in the background.

.loader {
	position: absolute;
	top:0;
	left: 0;
	width: 100%;
	height: 0;
	padding-top:27.1739%;
	background-image: url(largephotos/photo1.jpg);
	background-size: cover;
}

Ok, that is great, but what if you want to load more than one image on the page this way? Take a look at example 3. Here is the modified script at the bottom of the page…

<script>
	
	var BgImages = ['photo1.jpg', 'photo2.jpg', 'photo3.jpg'];

	for( var i=0; i<BgImages.length; i++){

		var image = new Image();

		image.src = 'largephotos/'+BgImages[i];	

		imageLoad(i);

	}

	function imageLoad(incrementer){

		image.onload = function () {
			var theContainer = document.createElement('div');
			theContainer.style.backgroundImage = "url(largephotos/"+BgImages[incrementer]+")";
			theContainer.className="loader fade";
			document.getElementsByClassName('lazyload')[incrementer].appendChild(theContainer);

		}
	}
	
</script>

I manually built an array of images. This could be done programmatically, but for simplicity sake, I just made the array. Then the script loops through the array and creates a new image for each element in the array. At this point, the browser will start downloading all three images. Now the anonymous function has a name. This is necessary because of the way scope works in JavaScript. The incrementer is passed in so that each image can be added to the correct element.

Ok, that is cool, but what if you want to make it so that the images don’t start downloading until they are visible on the page. Why make users download images they will not see, if they decide not to scroll down? Also, no point in having a cool fade in effect if no one can see it. Take a look at the fourth and final example file.

The JavaScript here is a bit more complex, so we will build it in pieces…

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

	var pageTop = document.body.scrollTop || document.documentElement.scrollTop

	console.log(pageTop);

});

This event listener in the script at the bottom of the page will keep track of scrolling. The code on line 3 requires some explaining. ScrollTop is tracked differently in Chrome and Firefox (at least at this time). Chrome needs body.scrollTop, Firefox needs documentElement.scrollTop. Now it is pretty unusual to see an assignment statement like this, with the || or in it. Here is how it works: Depending on if you are in Chrome (webkit based browser) or Firefox (Mozilla based browser) one of those two statements will always be zero. JavaScript, when assigning a value to pageTop, will assign the one that has a value, since the other is essentially false. Weird, I know, but it works. Check it out in the console log.

var articles = document.getElementsByTagName('article');
var counter = 0;


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

	var pageTop = document.body.scrollTop || document.documentElement.scrollTop;

	if ( pageTop > articles[counter+1].offsetTop ) {

		counter++;
	}

	console.log(counter);

});

Add these two variables at the top of the script, then, in the script add an if statement that checks the position of offsetTop as we scroll for each article. The counter only gets incremented once we get past the top of the article. When we get to the bottom of the page, the counter increments to 3, which is beyond our article array, and so we start getting an error. Notice, when you run this in the Chrome console, the counter gets checked hundreds of times for each article. That poses a challenge.

Let’s deal with the error we get with the counter when we scroll past the top of the third article first.

var articles = document.getElementsByTagName('article');
var counter = 0;
var numOfArticles = articles.length;


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

	var pageTop = document.body.scrollTop || document.documentElement.scrollTop;

	if(counter < numOfArticles-1){

		if ( pageTop > articles[counter+1].offsetTop ) {

			counter++;
		}
	}

	console.log(counter);

});

Adding a variable to keep track of how many articles we have, and putting an IF statement in that only increments the counter if the counter is one less than the number of articles will do the trick. No more errors at the end of the page.

Now here is the tricky problem. I want to trigger loading each image once, based on the position of the scroll.

If I could set my loader to run when a certain pixel is hit during scrolling, that would work great. However, scrolling is somewhat unpredictable and I can be certain I will hit that specific pixel, especially if I am scrolling fast. If I set it for a range of pixels, then the loader will run multiple times while I am scrolling in that range, and I only want it to fire ONE time for each image as it scrolls onto the page.

Here is the solution I came up with…

var loaded = [];

for(var i=0; i<numOfArticles; i++){
	loaded.push(0);
}

At the top of the script, with the other variables, make a new one called loaded and make it an empty array. Then I used a for loop to push a zero into the array for each article on the page.

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

	var pageTop = document.body.scrollTop || document.documentElement.scrollTop;

	if(counter < numOfArticles-1){

		if ( pageTop > articles[counter+1].offsetTop ) {

			counter++;

			if(loaded[counter] == 0){
				loaded[counter]++;

				console.log("array change fired");
			}
		}
	}

//console.log(counter);

});

Then add one more IF statement to the event listener that will check to see if the value in the array is set to zero. If it is, increment it to 1. This way, the array will only be changed ONCE for each article, which is exactly what we need for our script. This is the hardest part. The rest is pretty much what we implemented in example 3.

var articles = document.getElementsByTagName('article');
var counter = 0;
var numOfArticles = articles.length;
var loaded = [];
// add the array for the images again...
var BgImages = ['photo1.jpg', 'photo2.jpg', 'photo3.jpg'];

for(var i=0; i<numOfArticles; i++){
	loaded.push(0);
}


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

	var pageTop = document.body.scrollTop || document.documentElement.scrollTop;

	if(counter < numOfArticles-1){

		if ( pageTop > articles[counter+1].offsetTop ) {

			counter++;

			if(loaded[counter] == 0){
				loaded[counter]++;

				//console.log("array change fired");

				// load the image for each article once when the image
				// gets to the top of the page
				var image = new Image();
				image.src = 'largephotos/'+BgImages[counter];
				imageLoad(counter, image);
			}
		}
	}

//console.log(counter);

});

// Add the imageLoad function back in...
function imageLoad(incrementor, image){

	image.onload = function () {
		var theContainer = document.createElement('div');
		theContainer.style.backgroundImage = "url(largephotos/"+BgImages[incrementor]+")";
		theContainer.className="loader fade";
		document.getElementsByClassName('lazyload')[incrementor].appendChild(theContainer);
	}

}

On line 6 I added the array for the photos back in. On line 30 put in the bit that actually loads the image when it gets to the top of the page. And finally, the imageLoad function is added at the bottom of the script. It has one additional argument, so that we can know which we want to load as the page scrolls.

The script is working well, except that the first image never loads. That is because it is already on the page when it loads and the user never scrolls to get to it. Also, I want the other images to load as they come into view, not when they get to the top of the page. Below is the slightly modified code and the final script.

var articles = document.getElementsByTagName('article');
var counter = 0;
var numOfArticles = articles.length;
var loaded = [];
// add the array for the images again...
var BgImages = ['photo1.jpg', 'photo2.jpg', 'photo3.jpg'];

// the height of the window, minus 200 pixels...
var height = window.innerHeight - 200;

for(var i=0; i<numOfArticles; i++){
	loaded.push(0);
}

// Load the first image...
var image = new Image();
image.src = 'largephotos/'+BgImages[counter];
imageLoad(counter, image);

// Load other images as they scroll into view...
window.addEventListener('scroll', function(){

	var pageTop = document.body.scrollTop || document.documentElement.scrollTop;

	if(counter < numOfArticles-1){

		if ( pageTop > articles[counter+1].offsetTop - height ) {

			counter++;

			if(loaded[counter] == 0){
				loaded[counter]++;

				//console.log("array change fired");

				// load the image for each article once when the image
				// gets to the top of the page
				var image = new Image();
				image.src = 'largephotos/'+BgImages[counter];
				imageLoad(counter, image);
			}
		}
	}

//console.log(counter);

});

// Add the imageLoad function back in...
function imageLoad(incrementor, image){

	image.onload = function () {
		var theContainer = document.createElement('div');
		theContainer.style.backgroundImage = "url(largephotos/"+BgImages[incrementor]+")";
		theContainer.className="loader fade";
		document.getElementsByClassName('lazyload')[incrementor].appendChild(theContainer);
	}

}

Line 9 gets the height of the user’s window and subtracts 200 pixels from it. This variable is then used on line 27 making it so that the image will fade in once it is 200 pixels up from the bottom of the user’s window.

To get the first image to load, I just added the original loading script on lines 16, 17 & 18. This is an exact copy of the code from lines 38, 39 & 40, but it will only load that first image.

That is it! You can download the files and play around with them. There are lots of ways this script can be modified to suit your specific needs. Have fun with it!

Comments are closed.

Categories

Recent Comments

    Featured Projects

      Warning: Undefined variable $this_post in /home/meadpoin/meaddesign.net/portfolio/wp-content/themes/portfoliotheme/single.php on line 89
      > ZAP Creative

      Responsive Design, HTML, CSS, JS, PHP and WordPress

      Warning: Undefined variable $this_post in /home/meadpoin/meaddesign.net/portfolio/wp-content/themes/portfoliotheme/single.php on line 89
      > Animal Shelter

      Responsive Design, HTML, CSS, JS, PHP and MySQL

      Warning: Undefined variable $this_post in /home/meadpoin/meaddesign.net/portfolio/wp-content/themes/portfoliotheme/single.php on line 89
      > Experimental JavaScript

      Responsive Design, HTML, CSS, and JavaScript

      Warning: Undefined variable $this_post in /home/meadpoin/meaddesign.net/portfolio/wp-content/themes/portfoliotheme/single.php on line 89
      > KITTENS FOR SALE!

      HTML, CSS & JavaScript focusing on jQuery plugins

      Warning: Undefined variable $this_post in /home/meadpoin/meaddesign.net/portfolio/wp-content/themes/portfoliotheme/single.php on line 89
      > Syllabus Generator

      PHP & MySQL Web Application

      Warning: Undefined variable $this_post in /home/meadpoin/meaddesign.net/portfolio/wp-content/themes/portfoliotheme/single.php on line 89
      > CodeSnap Web Application

      PHP & MySQL web application

    More Projects

      Warning: Undefined variable $this_post in /home/meadpoin/meaddesign.net/portfolio/wp-content/themes/portfoliotheme/single.php on line 106
      > 4.23 Microsite

      Experimental JavaScript Microsite

      Warning: Undefined variable $this_post in /home/meadpoin/meaddesign.net/portfolio/wp-content/themes/portfoliotheme/single.php on line 106
      > Audio Space

      HTML, CSS & jQuery

      Warning: Undefined variable $this_post in /home/meadpoin/meaddesign.net/portfolio/wp-content/themes/portfoliotheme/single.php on line 106
      > Bttrfly Productions

      HTML, CSS, JS, & WordPress

      Warning: Undefined variable $this_post in /home/meadpoin/meaddesign.net/portfolio/wp-content/themes/portfoliotheme/single.php on line 106
      > Desktop Repainter

      HTML, CSS & JS

      Warning: Undefined variable $this_post in /home/meadpoin/meaddesign.net/portfolio/wp-content/themes/portfoliotheme/single.php on line 106
      > Bluephant Dental

      HTML, CSS, & JS

      Warning: Undefined variable $this_post in /home/meadpoin/meaddesign.net/portfolio/wp-content/themes/portfoliotheme/single.php on line 106
      > Zen Music Festival

      HTML & CSS

    About William Mead…

    William Mead Photo

    This site shows some of my front-end design and development work and shows how I use these projects to teach students about those same topics.

    Find me on these networks

    Professional Qualities

    • Enthusiastic about teaching, learning, managing projects and building products for the web.
    • Deeply creative and always playful. Frequently engrossed in solving complex puzzles.
    • Thoroughly engaged in the design and web development industries.
    • A visual learner with an analytical mind.
    • Introspective and always striving for improvement.

    Skills

    I am particularly good at bending CSS to do my bidding, and I really enjoy creating custom interactivity with JavaScript. I am always excited about new problems that need to be tackled.

    Contact Me…

    I look forward to hearing from you. Please contact me using the form or directly by email or phone.

    bill@meaddesign.net
    530-219-8998

      Your Name (required)

      Your Email (required)

      Subject

      Your Message

      Anti Spambot Question