Jack Carey

Lazy Loading Resources

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

  • Pygments — adds syntax to code blocks during the build, which are stylised using specific CSS.
  • Lightbox — Loads images in a responsive overlay. Used for enlarging thumbnail sized previews of pictures.
  • Label Code — A script I wrote for this site that labels the language used in a code block and adds a link to hide/show them.
  • Links External — A script I wrote for this site that adds target=_blank to external links, so that they are styled and handled correctly by the browser.
  • Art.js — A script I wrote for this site that you can read about here. It renders the tile patterns that you can see at on the homepage and elsewhere.
Photo by Jackson So on Unsplash

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);
});

Jack Carey