Sitecore

Defer offscreen images in Sitecore

Typing on laptop

When chasing down performance problems on a website, you’ll often times hit an error around deferring offscreen images.  This warning occurs when you have imagery “below the fold” (e.g., the area you must scroll to see) loading on your webpages. This problem is especially rampant in CMS systems where you’re never quite sure what the content authoring team is assembling.

Defer Images

Unfortunately, images are critical. We can’t eliminate them. According to the state of images report, at the time of writing, the average webpage loads 29 images at 950kb. The report states that the average webpage can save an additional 300kb of load time if images are lazily loaded.

Luckily, there are a few tricks to solve this within Sitecore, although these principles extend to any website or CMS architecture.

Foreground Imagery vs Background Imagery

First off, when I talk about foreground imagery, I’m talking about any image loaded directly via an inline <img> tag within your HTML. This is the simplest kind of image, it’s HTML 101.

Background imagery, on the other hand, is imagery which may be loaded via the CSS background-image property. These are often large banner images used to build logical chunks of your webpages (think stripe component).

In both cases, what we need to do is craft our initial HTML response coming out of the server to have the proper markup so that Javascript can take over and perform the actual “loading” of images for us. This technique is nothing new, and you can read about it in depth on css-tricks.com.

Let’s start with the front-end pieces.

Lazy Loading the image tag

Traditionally, to serve an image you would have HTML output similar to this:

<img src="/path/to/my/image/file.png" />

The unfortunate part about this tag is that the browser will initiate a download of this image as soon as it sees this HTML snippet, regardless of where this tag is on the page. We can prevent that download by transforming the tag slightly:

<img data-src="/path/to/my/image/file.png" class="lazy" />
Sitecore - Understanding Development Approaches: A Sitecore Outlook
Understanding Development Approaches: A Sitecore Outlook

Designing, building, and implementing top-notch experiences not only requires a great deal of planning, strategy, and time – it also requires the right digital experience platform (DXP) and the right development approach for your business needs.

Get the Guide

The data-src treatment will prevent the browser from automatically loading this image. Keep in mind that this will result in what appears to be a broken image if the user actually sees it (unless you style .lazy accordingly). If this is a problem you can also take a secondary, but similar approach:

<img src="/path/to/a/really/small/placeholder.jpg#/path/to/my/real/image.png" class="lazy" />

In this second approach, we’re actually loading a real image (placeholder.jpg), but this “real” image can be as simple as a 1px by 1px white box. The overall method is still the same.

Essentially, whenever the image comes into view, we need to use a bit of javascript to replace “data-src” with “src”. In doing so, this will trigger the browser to download and paint the image immediately.

function lazyload() {
    if (lazyloadThrottleTimeout) {
        clearTimeout(lazyloadThrottleTimeout);
    }

    lazyloadThrottleTimeout = setTimeout(function () {
        var lazyElements = document.querySelectorAll(".lazy");

        var scrollTop = window.pageYOffset;
        Array.prototype.forEach.call(lazyElements, function (elem) {
            // within 100px of the bottom of the screen
            if (elem.offsetTop - 100 < (window.innerHeight + scrollTop)) {
                if (elem.dataset.src) {
                    // find any data-src and switch it to src
                    elem.src = elem.dataset.src;
                }
                elem.classList.remove('lazy');
            }
        });
        if (lazyElements.length == 0) {
            document.removeEventListener("scroll", lazyload);
            window.removeEventListener("resize", lazyload);
            window.removeEventListener("orientationChange", lazyload);
        }
    }, 20);
}

document.addEventListener("scroll", lazyload);
window.addEventListener("resize", lazyload);
window.addEventListener("orientationChange", lazyload);
lazyload(); // go ahead and invoke on page load

Again, this javascript is nothing new. You can see a similar example on css-tricks.com or within this codepen. We’re essentially looping through all elements with the class of “lazy” and processing the ones that are currently visible on the screen. Processing involves swapping data-src for src and removing the class of “lazy”.

Rendering (foreground) images lazily

Up until this point, everything you’ve seen revolves around pure HTML and JS. Luckily, the Sitecore CMS is flexible enough to allow us full control over how HTML is rendered, so we can absolutely accommodate our new front-end trick. Rather than attempt to manually render images by hand, what we want to do is leverage the existing @Html.Sitecore().Field(…) helper methods and extend them to always render images with the data-src attribute. To do it, we’ll patch into the renderField pipeline.

<?xml version="1.0" encoding="utf-8"?>
<configuration xmlns:role="http://www.sitecore.net/xmlconfig/role/" xmlns:patch="http://www.sitecore.net/xmlconfig/" xmlns:set="http://www.sitecore.net/xmlconfig/set/">
  <sitecore>
    <pipelines>
      <renderField>
        <processor role:require="ContentDelivery or Standalone"
                   patch:before="processor[@type='Sitecore.Pipelines.RenderField.GetImageFieldValue, Sitecore.Kernel']"
                   type="MySite.Pipelines.RenderField.LazyImageRenderer, MySite.Custom" 
                   />
      </renderField>
    </pipelines>
  </sitecore>
</configuration>

Drop this into your website’s Pipelines.config file (or make one if you don’t have one). The renderField pipeline gets executed every time Sitecore attempts to render an image. Each processor within the pipeline checks the field type to render, and generates the necessary HTML if possible. Once HTML is generated, the pipeline is exited. Traditionally, each processor is responsible for a particular field type. In our case, we’re patching a processor in place before the default Sitecore GetImageFieldValue processor (responsible for generating the <img> tag markup).

Our processor is quite simple. We can inherit from Sitecore’s GetImageFieldValue processor to do most of the heavy lifting for us. From there, all we really need to do is alter the results being written to the HTTP response:

public class LazyImageRenderer : Sitecore.Pipelines.RenderField.GetImageFieldValue
{     
    public DeferedImageRenderer()
    {
    }

    public virtual void Process(RenderFieldArgs args)
    { 
        Assert.ArgumentNotNull((object)args, nameof(args));
        if (!this.IsImage(args)) // base utility method
            return;

        if (this.ShouldExecute())
        {
            base.Process(args); // generate "default" result
            args.Result.FirstPart = this.FixImageTag(args.Result.FirstPart); // alter results
            args.AbortPipeline(); // exit out
        }
    }
    
    
    public bool ShouldExecute()
    {
        if (!Sitecore.Context.PageMode.IsNormal)
            return false;

        if (Sitecore.Context.Site == null)
            return false;

        if (RenderingContext.Current?.Rendering?.RenderingItem?.InnerItem == null)
            return false;

        // note: you have access to RenderingContext.Current.Rendering at this point in time
        // you could check this for the current placeholder or type of rendering being processed
        // this may be useful if you want to avoid lazy loading images within your header, or
        // prevent lazy loading images within your hero renderings.
        
        return true;
    }
    
    public string FixImageTag(string tag)
    { 
        // swap src= for data-src= to trick the browser into ignoring this image tag
        tag = tag.Replace("src=", "data-src=");

        // important: inject a class of "lazy" to ensure Javascript can lazily load this image
        if (tag.Contains("class=\""))
        {
            tag = tag.Replace("class=\"", "class=\"lazy ");
        }
        else
        {
            tag = tag.Replace("/>", "class=\"lazy\" />");
        }

        return tag;
    }
}

As you can see, we’re simply replacing “src” with “data-src” and injecting the CSS class of “lazy” in place. Easy enough, right?

One important note here: it may or may not be wise for you to defer all images on your website. If you can, try and detect and lazily load only the images which are below the fold. This may not be possible in all scenarios, but checking RenderingContext.Current.Rendering against a list of known Hero renderings is a good way to know if this should or shouldn’t execute. Another option may be to create an “above the fold” placeholder for all pages. If all else fails, it may be wise to lazy load all imagery if your pages are long and this error must be fixed- but if your pages are short and you only have a handful of below the fold images, lazy loading may cause more harm than good.

Also take note that Javascript is required for this to work, so if supporting non-javascript enabled experiences are a must, you shouldn’t implement this (although that could be another check if, perhaps, you check the current HttpContext for a flag set by javascript).

Rendering Background Images Lazily

Up until this point, I’ve mostly talked about Foreground imagery. So what about background imagery?

Background imagery simply looks like this:

<div style="background-image: url('/path/to/some/banner/image.jpg')">
   <!-- ... -->
</div>

This is where the power of CSS comes in really handy. Remember our Javascript snippet earlier? It’s indiscriminately looking for all instances of the class “lazy” on the page, regardless of tag type, and is removing the class as they scroll into view. We can leverage this to our advantage with this CSS:

<style type="text/css">
    .lazy {
        background-image: none !important;
        transition: background-image ease-out 0.1s;
    }
</style>

Be sure to put this directly within the <head> of your HTML (NOT within a css file), and ensure that this renders before any other images on the page. Forcing background-image: none !important; will override all other in-lined background-image styles on the website. Whenever the lazy class is removed, the inline style will take over and the image will load. We typically throw a bit of spice on top with the ease-out css as well. This means that our above example simply needs to be modified to:

<div class="lazy" style="background-image: url('/path/to/some/banner/image.jpg')">
   <!-- ... -->
</div>

Now, another quick tip is to apply a similar technique to your foreground images:

<style type="text/css">
    img.lazy {
        opacity: 0;
        transition: opacity ease-out 0.1s;
    }
</style>

Put this in the head of your document as well, and it will ensure that any of your lazy images won’t appear “broken” if the user happens to scroll one into view before their network can catch up and load the image.

Again, shout out to css-tricks.com for sharing their wonderful guide on lazy loading, and I hope this helps you within your Sitecore journey.

About the Author

I am a certified Sitecore developer, code monkey, and general nerd. I hopped into the .NET space 10 years ago to work on enterprise-class applications and never looked back. I love building things—everything from from Legos to software that solves real problems. Did I mention I love video games?

More from this Author

Leave a Reply

This site uses Akismet to reduce spam. Learn how your comment data is processed.

Subscribe to the Weekly Blog Digest:

Sign Up