We’re fans of Custom Elements around here. Their design makes them particularly amenable to lazy loading, which can be a boon for performance.
Inspired by a colleague’s experiments, I recently set about writing a simple auto-loader: Whenever a custom element appears in the DOM, we wanna load the corresponding implementation if it’s not available yet. The browser then takes care of upgrading such elements from there on out.
Chances are you won’t actually need all this; there’s usually a simpler approach. Used deliberately, the techniques shown here might still be a useful addition to your toolset.
For consistency, we want our auto-loader to be a custom element as well — which also means we can easily configure it via HTML. But first, let’s identify those unresolved custom elements, step by step:
class AutoLoader extends HTMLElement
connectedCallback()
let scope = this.parentNode;
this.discover(scope);
customElements.define(“ce-autoloader”, AutoLoader);
Assuming we’ve loaded this module up-front (using async is ideal), we can drop a <ce-autoloader> element into the <body> of our document. That will immediately start the discovery process for all child elements of <body>, which now constitutes our root element. We could limit discovery to a subtree of our document by adding <ce-autoloader> to the respective container element instead — indeed, we might even have multiple instances for different subtrees.
Of course, we still have to implement that discover method (as part of the AutoLoader class above):
discover(scope)
let candidates = [scope, …scope.querySelectorAll(“*”)];
for(let el of candidates)
let tag = el.localName;
if(tag.includes(“-“) && !customElements.get(tag))
this.load(tag);
Here we check our root element along with every single descendant (*). If it’s a custom element — as indicated by hyphenated tags — but not yet upgraded, we’ll attempt to load the corresponding definition. Querying the DOM that way might be expensive, so we should be a little careful. We can alleviate load on the main thread by deferring this work:
connectedCallback()
let scope = this.parentNode;
requestIdleCallback(() =>
this.discover(scope);
);
requestIdleCallback is not universally supported yet, but we can use requestAnimationFrame as a fallback:
let defer = window.requestIdleCallback