Modern JavaScript via ES Module imports, provides us with two ways of handling modular JavaScript. There's import
whatever from where-ever
style, and then there's import()
. While minor in syntax difference, did you know they have a direct impact on the loading of your application? Let's look:
// knights-who.js
import "./the-parrot-sketch.js";
// really important class that says..
class KnightsWho extends HTMLElement {
constructor() {
super();
if (this.getAttribute("say") != null) {
let sketchTag = document.createElement("the-parrot-sketch");
sketchTag.innerHTML = this.getAttribute("say");
this.appendChild(sketchTag);
}
}
}
customElements.define("knights-who", KnightsWho);
Then your main.html
document might reference this really important modular JavaScript as follows:
<script type="module" src="knights-who.js"></script>
<knights-who say="Nee"></knights-who>
In this syntax, the browser responds with the following data cascade timing...
- GET
main.html
, start parsing - See script type="module" start requesting
knights-who.js
- Reads file for additional import references, finds
the-parrot-sketch.js
and requests that - Reads file for additional import references, endlessly until there are no more additional modular references
- Completes modular chain of code, executes all at once,
-
knights-who
tag will say Nee, wrapped in a<the-parrot-sketch>
tag; horrifying.
This is how modular JavaScript works though, it spiders out looking for additional modular import
references and then once all of them load, it executes all of them. This is great for developers to ship modular code, but what if you had ~100 references nested in other references?
"One Weird Trick" Dynamic import()
A dynamic import()
could be leveraged in our constructor()
to visually look similar, yet have a very different execution timing.. Let's look.
// knights-who.js
// really important class that says..
class KnightsWho extends HTMLElement {
constructor() {
super();
if (this.getAttribute("say") != null) {
let sketchTag = document.createElement("the-parrot-sketch");
sketchTag.innerHTML = this.getAttribute("say");
this.appendChild(sketchTag);
setTimeout((e) => {
import("./the-parrot-sketch.js");
}, 0);
}
}
}
customElements.define("knights-who", KnightsWho);
In this setup, we use import()
inside of our constructor(). By doing this, we get the following timing on spin up.
- GET
main.html
, start parsing - See script type="module" start requesting
knights-who.js
- Reads file for additional import references, finds none.
- Completes modular chain of code, executes all at once,
-
knights-who
tag will say Nee, wrapped in a<the-parrot-sketch>
tag (undefined). So it starts to paint while in the background, delayed one microtask,./the-parrot-sketch.js
read off endlessly until there are no more additional modular references, but the tag is imported on it's own schedule!
The key difference here is that we've started to paint potentially long before we would have otherwise by breaking our chain into multiple execution chains! While small in a single element, imagine building a whole application where every step of the way you handled info this way.
Here's a gif showing this happening at scale in HAXcms as loaded on haxtheweb.org. Loading has been throttled to 3G to demonstrate, but all the parts of the UI are web components and all parts load in via a series of broken up import()
chains to optimize delivery.
Considerations
This breaks up timing so you could get a FOUC if there is a non-hydrated element that has spacing considerations (which is likely). In the .gif
above a chunk was cut out that was just a white screen as we need to fix a our loading indicator's timing to avoid FOUC 😳. But, even with this, we don't actually flash unstyled content as we currently just have a loading bar that goes until the UI is ready. Individual UI elements then have sane sizing defaults using a css selector trick of :not(:defined) {}
which helps w/ selecting web components that do not have a definition (yet).
The import()
methodology is to speed up time to first paint (TTFP) and so you could use some sizing styles or css or stateful variables internal to the import in order to reduce FOUC. We'll go into dynamic import Promise
later but here's a taste:
connectedCallback() {
this.setAttribute("hidden", "hidden");
import("./what-ever.js").then((m) => { this.removeAttribute("hidden")});
}
While simplistic, this would allow the whole application / other elements to keep loading in the background while the user still obtains part of the experience. connectedCallback
means we it is attached to the DOM and thus we can start setting attributes. This code would "paint" the element, then hide it, then when the internals of what-ever.js
have loaded, it would reveal the entire element.
Top comments (4)
Can you explain what
sketchTag
does in this code. And where doessay
come from inthis.innerHTML = say;
?lol. Why yes I can! By gaslighting you completely and saying "but it really says
this.innerHTML = sketchTag;
". In this, we can see that you have found a mistype and I hope that now the update makes more sense :).The point of
sketchTag
was simply the idea of another tag providing the advanced functionality. aaaannnnnd then I didn't actually set it to innerHTML (a horribly simplistic method of making this load) :)Thanks for the note. Updated both doc blocks.
Dont gaslight me, i fart pure alcohol. Code is wrong... your trying to stuff dom elements into a string
The following code was always there, under every comment that was made, and has never been modified from it's original, posted below.
room explodes