Skip to main content

Architecture

Defer offscreen images in Episerver

Abstract Lights

Lazy loading images is a technique for modern web developers where you instruct the client’s browser to only download images as they are needed.  This leads to tremendous performance improvements, as client devices do not waste bandwidth downloading assets which are not being rendered.  To achieve this, we’ll use some client side mechanisms from css-tricks.com and support them with proper markup from the Episerver CMS.

Foreground Imagery and Background Imagery

When talking about Imagery used on websites, we need to think of ways in which this imagery is actually used.  A foreground image is your traditional image tag <img> which you learn in HTML 101.  This is the standard way in which an image is rendered on screen.

Background imagery, however, is an image which is loaded via CSS and applied through styling.  Think of a <div> tag with a background-image property inlined on it.  This is typically used for large banner imagery with text over-lays.

In both cases, what we need to do is craft our HTML response coming from the server to support a lazy image processing technique in Javascript.

Front-end Mechanics

The standard way in which you would serve up an image on your website would be to implement an <img> tag, like so:

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

The unfortunate reality behind this image tag, is that as soon as the client’s browser sees it- it will attempt to load the asset at the src attribute.  Even if this image is located in the footer of a lengthy page- it will be downloaded immediately.

To prevent this download, what we can do is output the markup in a different manner from the server side:

<img data-src="/path/to/my/file.png" class="lazy" />

Notice in this case that we didn’t output a src property at all.  Instead, we output data-src.  The browser doesn’t know how to handle this, and typically it will render this img tag as a “broken thumbnail” (if we allowed it).  Because we put a CSS class of .lazy on the image, we can style this image to be hidden just-in-case someone scrolls it into view without Javascript.

Within the Head section of your document, add this to your Critical CSS:

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

Now comes the fun part- we need to execute a bit of client side Javascript to actually process our images and trick the browser into loading them as they scroll into view.  To do this, use a code snippet like this:

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

In this case, we’re defining a function lazyload() which is used as a callback for the scroll, resize, and orientationChange events.  We also directly invoke this function on page load, just in case any deferred images are located above-the-fold.  All this Javascript does is loop through the DOM and find every element with a class of lazy.  For each object found, if it is within the viewport, the lazy class is removed and data-src is swapped for src.  This is the core of lazy loading images, and from here on out we just need to craft our server side responses accordingly.  As an interesting side note- it can be incredibly difficult to determine the client device’s viewport from the server side, so I recommend only lazy loading images which are located after the first ~1000px of screen height.

Also take note, that for our background images it’s the same technique.  We need to take this markup:

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

and instead, render it like so:

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

With the CSS we inlined in the head of our documents, the .lazy class will take precedence and force the background image property to none !important;.

Back-end Episerver Techniques

Up until this point we’ve talked about how to get lazy loading to work on the front-end.  This technique applies to a large variety of web technologies, and can be done with a multitude of platforms.  All we have to do is get the back-end to render the markup accordingly.

Luckily within Episerver, controlling the way in which Images render is incredibly easy.  First, we must ensure we have a DisplayTemplate created for our Image files: ~/Views/Shared/DispalyTemplates/Image.cshtml.  This file will instruct Episerver on how to render the markup for an Image.  In my case, Image.cshtml looks like this (assuming we’re following along with Alloy…):

@using EPiServer.Editor
@model ImageViewModel

@if (Model != null)
{
    var lazy = ViewBag.lazy ?? true;

    if (PageEditing.PageIsInEditMode || !lazy)
    {
        <img src="@Model.Url" />
    }
    else
    {
        <img data-src="@Model.Url" class="lazy" />
    }
}

The critical piece here is that I’m passing a flag of “lazy” into the ViewBag to control if the image should be lazily loaded or not.  This is important for elements such as Heroes, where they are always present above-the-fold and you want them to load as fast as possible.

Here’s how you can use it:

@* Won't be lazy loaded... *@
@Html.PropertyFor(x => x.CriticalImage, new { lazy = false })

@* Will be lazy loaded... *@
@Html.PropertyFor(x => x.LazyImage)

Fixing the Episerver Rich Text

One thing to take note of: the Episerver Rich Text TinyMCE editor doesn’t use the Display Templates for rendering imagery.  It has it’s own internal mechanisms to do this.  Luckily, we can get around this by applying a similar technique- creating a Display Template for XhtmlString property types.

Create ~/Views/Shared/DispalyTemplates/XhtmlString.cshtml to gain control of the way rich text is rendered, and add some code similar to this:

@using EPiServer.Core
@model XhtmlString

@Html.Raw(Model.FormatRichText(ViewContext))

From here, our FormatRichText() extension method can handle rendering the markup, grabbing the result, and adjusting it before sending it out to the client.  I use HtmlAgilityPack to parse the HTML result.  Here’s my extension method:

public static class XhtmlStringExtensions
{
    /// <summary>
    /// Parses the XhtmlString for Image tags and sets them to lazy load
    /// </summary>
    public static string FormatRichText(this XhtmlString html, ViewContext context)
    {
        // html is null when the TinyMce is not initialize (creating new block etc.)
        if (html == null) return string.Empty;

        // Load up Epi's HtmlHelper and ask it to render results
        var hh = new HtmlHelper(context, new ViewPage());

        string epiRenderingResult;

        using (var writer = new StringWriter())
        {
            hh.ViewContext.Writer = writer;
            hh.RenderXhtmlString(html);
            writer.Flush();
            epiRenderingResult = writer.ToString();
        }

        if (PageEditing.PageIsInEditMode)
            return epiRenderingResult;

        // once results are rendered, load up HtmlAgilityPack and have it parse results

        var doc = new HtmlDocument();
        doc.LoadHtml(epiRenderingResult);

        // find all image tags, if there are none just return out- otherwise we need to adjust them

        var imgs = doc.DocumentNode.SelectNodes("//img");
        if (imgs == null)
            return epiRenderingResult;

        // for each image, swap src for data-src and throw "lazy" class on it
        foreach (var img in imgs)
        {
            var src = img.Attributes["src"]?.Value;
            if (!string.IsNullOrEmpty(src))
            {
                // to support lazy loading, we need to swap src for data-src
                // and inject a class of "lazy".  Javascript will take it from there
                img.SetAttributeValue("data-src", src);
                img.Attributes.Remove("src");

                var css = img.Attributes["class"]?.Value;
                img.SetAttributeValue("class", $"lazy {css}".Trim());
            }
        }

        var outerHtml = doc.DocumentNode.OuterHtml;
        return outerHtml; // return out the new resulting HTML
    }
}

Enjoy 🙂

Thoughts on “Defer offscreen images in Episerver”

  1. Instead of all this javascript, you could simply use webstandards and put a loading=”lazy” on your image tags and it will just work in pretty much all modern browsers. You could choose to overwrite Episervers shared display templates where needed if you wanted to overwrite how @Html.PropertyFor(..) rendered images, no need for scripting here.

Leave a Reply

Your email address will not be published. Required fields are marked *

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

Dylan McCurry, Solutions Architect

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

Follow Us
TwitterLinkedinFacebookYoutubeInstagram