Lazy Loading Resources

JavaScript tag🌐

I’ve been rebuilding this website with Jekyll and found myself using a few different resources as part of the process:

As these resources aren’t absolutely crucial to rendering the page or interacting with it, I chose to load them after the rest of the site. At first I was using async or defer on the script tags, then I decided to add a self-invoking function to the end of the body that added the relevent elements to the DOM. This improved load times and Lighthouse scores but I realised I could do more. There are some pages where the scripts or styles simply aren’t needed at all. I knew there would be conditions for loading the scripts, but these aren’t always possible to check for in the Jekyll build process. By adding a prerequisite function to the window load event, I can use the compiled DOM to check if each page needs a resource.

The script below is 965 bytes minified, but it’ll stop ≤23.6kB from being loaded unnecessarily across up to 5 requests. For larger or more complicated sites the gains could be even greater so I’ll definitely be applying this pattern to websites I make in the future.

/*
 * Author: Jack Carey (jackcarey.co.uk)
 * License: GPL-3 (https://www.gnu.org/licenses/gpl-3.0.en.html)
 * Version: 2
 * Description: Load resources at some later stage so they are not render blocking.
 * Each object should contain the attributes that will be applied to the link or script elements.
 * 'prereq' is optional and must be a function.
 *  - The resource will only be loaded if it returns a truthy value.
 *  - It is checked on window load.
 * Performance is the goal here so you should minify this file and your resources as part of your build process.
 */
let links = [
    {
        href: "/css/pygments.min.css",
        type: "text/css",
        rel: "stylesheet",
        prereq: () => {
            return document.querySelector("code") != null;
        },
    },
    {
        href: "/css/lightbox.min.css",
        type: "text/css",
        rel: "stylesheet",
        prereq: () => {
            return document.querySelector("a img") != null;
        },
    },
];
let scripts = [
    {
        src: "/js/lightbox.min.js",
        type: "text/javascript",
        prereq: () => {
            return document.querySelector("a img") != null;
        },
    },
    {
        src: "/js/label-code.min.js",
        type: "text/javascript",
        prereq: () => {
            return document.querySelector("code") != null;
        },
    },
    {
        src: "/js/links-external.min.js",
        type: "text/javascript",
    },
    {
        src: "/js/art.min.js",
        type: "text/javascript",
        prereq: () => {
            return document.querySelector(".splash-art") != null;
        },
    },
];
function addResource(appendTo, tagName, obj) {
    let elem = document.createElement(tagName);
    let loadResource = () => {
        Object.keys(obj).forEach((key) => {
            if (typeof obj[key] !== "function") {
                console.log(key);
                elem[key] = obj[key];
            }
        });
        appendTo.appendChild(elem);
    };
    let prereq = obj.prereq ? obj.prereq : null;
    if (prereq) {
        //load the resource on load if the prerequisite is true
        window.addEventListener("load", (ev) => {
            if (prereq()) {
                loadResource();
            }
        });
    } else {
        //load the resource now
        loadResource();
    }
}
links.forEach((obj) => {
    addResource(document.head, "link", obj);
});
scripts.forEach((obj) => {
    addResource(document.body, "script", obj);
});