For those who are not familiar, lazy loading means to only load in the content that is inside the users viewport, when the user scrolls down more content is loaded in. This has a number of benefits:
- Reduces load on the server, especially on taller single page sites
- Saves bandwidth for the user, especially good for those users on limited and often expensive data plans.
- The load event will fire sooner. While technically the page load time has been reduced it won’t be so noticeable to the user, as the page will render before all the images have downloaded.
N.B. Never omit the var keyword or else that variable will instead be bound to the global scope. This is bad.
Next we need to bind a scroll event handler to the window object, declare an anomynous function to be called on the scroll event. Then store the current window Y position &nbash; in other words how far we have scrolled from the top of the document. We also store the height of the window, and use that value to determine the position of the bottom of the window. This will be used to see if the images are inside the viewport.
Loop through all the lazy images by targeting the
.js-lazy-load hook, inside
of the loop we store the images’ offset and compare that with the window bottom
offset to determine if the image is within the viewport.
We also store the lazy-src attribute value.
Finally inside the if statement we set the url of the src attribute to the lazySrc value.
So that is a basic lazy load function. It works but there’s huge room for improvement in terms of performance, the poor performance of this function would be clearly noticeable if there’s many lazy-loaded images or other effects bound to scroll events such as parallax. This would be even more profound on lower powered devices.
##Micro optimisations## There are a few things we can do to the code to slighty improve the speed of execution..
Declare scrollOffset, winHeight and windowBottom offset outside of the scroll event.
inside the jQuery each loop we access
$(this) three times, we should store
$(this) inside of a variable for faster access.
These mico optimisations will help speed up the execution but compared to other optimisations we can make, their influence will be minimal, still it’s good to follow best practices and code sensibly in this manner.
##DOM interactions are the enemy of performance## Any query or manipulation of the DOM is very expensive, if we are building with performance in mind we need to ensure we need to reduce the amount of DOM interraction. First off the scroll event fires incredibly rapidly, this can be clearly seen if you console log out a value on the scroll event.
There are a number of ways to reduce this, one of them is known as throttling where we create a debounce function to reduce the number of times the callback function in the scroll event is called.
But my preferred method for this is just simply using a setInterval to fire every 200ms, then we could compare the current scroll position with the previous to determine if the user has scrolled.
The code below implements this, and you will see the scroll position is only logged to the console if the user has scrolled, while being simultaneously throttled. The reason I choose the setInterval method is because most mobile browsers, especially those on iOS will ignore scroll events during the scroll animation. This circumvents that issue.
Now we can combine this with the lazy load code function we wrote earlier.
So now the scroll is throttled , this in iself greatly improves the performance
but we are still making DOM querys. Every time the
lazyLoad() function is called
we have to calculate the images offset. It would be more efficient to determine the
offset once and store that value.
##Determining the position of the images## Initially this sounds very simple, on page-load we can just grab the offsetTop of the images and store the values in the array, but upon further investigation this presents a problem. When we scroll to an image and it loads in, it pushes the content below further down the page, rendering the remaning offset values invalid.
Responsive design makes this a little more complex because the images can scale
to their container with
max-width: 100%; and
It is possible to make a HTML block element maintain it’s aspect ratio when scaled using the padding-bottom percentage hack. This is how we will build our placeholder.
Lets create the necessary CSS for this to work. We force the image to be
contrained to the wrapping element’s proportions. The padding-bottom and width
to be added to
there via jQuery’s `.css method.
First loop through all the lazy-load images, get the width and height attributes
and use this to calculate the aspect ratio as a percentage which then gets applied
to the parent element in the padding-bottom property, then that element’s parent
gets it’s max-width attribute augmented with the images width.
This forces the block to behave like a responsive image of those dimensions.
So we now have our placeholders.
The full code should now look like this:
##Precalculating the offsets## One benefit I forgot to mention of the placeholder is that once the image loads the page will not have to be repainted unlike before because the placeholder already takes up that space so another +1 for the performance, but we’re not there yet.
We’re still calculating the image offset with every scroll. Lets determine those in a seperate function and store that data in an array that can be accessed when we scroll.
It’s Much faster to loop through an array of values than it is to pull them from the DOM.
First we have to set an empty array that can be later accessed
Next we should augment the
generatePlaceholders() function, while inside the
each loop we should also get the image imgOffset
and lazySrc` values,
then we can push the element reference, imgOffset and lazySrc as a single object
to the images array which can be accessed later then the user scrolls.
Maybe we should now rename this function to initialize seen as generatePlaceholders is no longer descriptitve of it’s function.
Now we can re-write the
lazyLoad() function, instead of querying the DOM
for the images we loop through the images array and test the stored offset value
against the window scroll positon, if the element is within the viewport it loads
This is now far more optimised than it was previously, but there’s a little more we can do. After an image has been loaded there’s no need to keep looping over it’s data in the array, so we should remove it from the array.
We can do that with the
##Putting it all together##
This is now pretty much about as optimised as it can get. We could offer different image sizes for smaller screens, but that is beyond the scope of this post. I’m currenlty running this on projects that include parallax effect and everything now typically runs at 60FPS or better. My old lazy-loading script was the bottleneck stopping me from acheiving that kind of frame rate.
Here is all the completed code:
N.B. The module should, of course be placed within a DOM ready event.
If anybody has anything to add, pehaps some way to optimise things even further then please leave a comment below. Thanks for reading.