DEV Community

Cover image for Lazyload Scripts like Images
paulbrowne
paulbrowne

Posted on

Lazyload Scripts like Images

One of the best improvements to html over the recent years was the loading="lazy" attribute that you can add to images (also iframes) that will tell the browsers to not load the image until it is in the viewport.



    <img src="/images/your-image.png" loading="lazy">


Enter fullscreen mode Exit fullscreen mode

Very simple, very useful. But wouldn't it be great if you could do this for scripts as well. So that you could lazily load your components, only if/when they are actually needed...

Well, one other feature the <img> element has, is the ability to run a script after the image loads (or doesn't load) with the onload and onerror attributes.



    <img 
        src="/images/your-image.png" 
        loading="lazy"
        onload="() => console.log('image loaded')"
    >


Enter fullscreen mode Exit fullscreen mode

This onload "callback" will only be fired when that image is loaded, and if the image is been lazily loaded, then it will fire only when the image is in the viewport. Et voilà! A Lazily loaded script.

Unfortunately, like this, it isn't much use. Firstly you'll have a unwanted image on your page, and secondly, you'll need to inline the javascript you want to run, which kinda defeats the purpose of lazy loading. So, lets make some changes to improve on this.

The image itself can be anything, or, more importantly, nothing. As I mentioned earlier there is the onerror callback, which - as the name would suggest - will fire when the image doesn't load.

This doesn't mean you need to point the src to a non-existent image, that would result in a console full of red 404 errors about missing images, and nobody wants that.

The onerror callback also fires if the src image is not actually an image, and the easiest way to do that is to "badly encode" an image using the data: format. This also has the benefit of not filling the console with warnings of missing images 👍



    <img 
        src="data:," 
        loading="lazy"
        onerror="() => console.log('image not loaded')"
    >


Enter fullscreen mode Exit fullscreen mode

This will still result in the page having the "broken image" thumbnail, but we'll get to that.

Ok, But we still need to inline the javascript we want to run, so how do we fix that?

Well, now that ES module support is almost universal, we can use the very powerful event-import-then-default javascript loading technique to load a script after an event has fired, like so:



    <img 
        src="data:," 
        loading="lazy"
        onerror="import('/js/some-component.js').then(_ => _.default(this))"
    >


Enter fullscreen mode Exit fullscreen mode

Note: This also works for onclick, onchange, etc. events
Note: The underscores are just shorthand way to access the Module, you could also write .then(Module => Module.default(this))

Ok, so what is going on here!?

First lets take a look at what some-component might look like:



// some-component.js

export default element => {
    element.outerHTML = `
        <div class="whatever">
            <p>Hello world!</p>
        </div>
    `;
}


Enter fullscreen mode Exit fullscreen mode

So, you might have noticed that in the onerror callback, I passed this as an argument to the default export. The reason I did this (excuse the pun 😁) was to give the script the <img> that called it, since in this (I did it again 🤦) context this = <img>.

Now you can simply element.outerHTML to replace the broken image with your html markup and there you have it, lazyloaded scripts! 😱

Caching, and Passing Arguments

If, you are using this technique more than once on a page, then you'll need to pass a "cache-busting" index, or random number to the data:, eg, something like:



    <img 
        src="data:,abc123" 
        loading="lazy"
        onerror="import('/js/some-component.js').then(_ => _.default(this))"
    >
    <img 
        src="data:,xyz789" 
        loading="lazy"
        onerror="import('/js/some-other-component.js').then(_ => _.default(this))"
    >


Enter fullscreen mode Exit fullscreen mode

The string after the ":," can be anything, just so long as they are different.

A very simple way to pass params to the function would be to use the data-something attribute in the html like so:



    <img 
        src="data:," 
        loading="lazy"
        data-message="hello world"
        onerror="import('/js/some-component.js').then(_ => _.default(this))"
    >


Enter fullscreen mode Exit fullscreen mode

Since we are passing the this to the function, you can access the data attributes like so:



export default element => {
    const { message } = element.dataset
    element.outerHTML = `
        <div class="whatever">
            <p>${message}</p>
        </div>
    `;
}


Enter fullscreen mode Exit fullscreen mode

Please let me know what you think in the comments! ❤️

Top comments (0)