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">
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')"
>
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')"
>
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))"
>
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>
`;
}
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))"
>
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))"
>
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>
`;
}
Please let me know what you think in the comments! ❤️
Top comments (0)