uwc bonus: HAX / HAXcms unbundled land
Bryan Ollendyke
Posted on July 22, 2020
HAX and HAXcms leverage the world of dynamic hydration in the following way to cheat on application performance:
HAX
HAX gets its mode of operation entirely from a appstore.json
file, example here. This file contains a section called autoloader
which inspired the entire hydration concept.
autoloader
defines the relationship of tag name to location. At run time, HAX loads these definitions in to provide an editing experience. The cool thing about dynamic imports are two-fold:
- They break up the execution chain so that you can do module imports but have them resolve later on in the paint of the page. This helps deliver all assets via modular chains but have some fire off later than others.
- They return a
Promise
meaning that we can execute functionality in athen
context, so only AFTER the file has been completely resolved (it's modular chains all loaded) and the code executed for the file. You can see how we leverage this capability in HAX to read off thehaxProperties()
schematic definition of how the element can interface with HAX here.
This approach allows us to not just load custom web components per deployment / context / use case, it allows for the page to start painting the theme layer of the application, then the HAX editor, and THEN as the user can interact with the DOM, the definitions of the potential elements to insert keep loading. This saves a ton of time in waiting for "the app" to load as it has loaded and the user can interact with it long before the unknown list of all possible elements has loaded.
HAXcms
In HAXcms, we took this approach to the next level. Instead of just dynamically importing the assets that can be placed in the UI but actually dynamically loading the theme layer, then via Promise
loading the content.
When the route changes in HAXcms, we fetch the page contents (which are always stored as individual html blobs). When this content is inserted, we have the following method which fires (seen here in the haxcms-site-builder tag) in order to process:
if (
!window.WCAutoload &&
varExists(this.manifest, "metadata.node.dynamicElementLoader")
) {
let tagsFound = findTagsInHTML(html);
const basePath = this.pathFromUrl(
decodeURIComponent(import.meta.url)
);
for (var i in tagsFound) {
const tagName = tagsFound[i];
if (
this.manifest.metadata.node.dynamicElementLoader[tagName] &&
!window.customElements.get(tagName)
) {
import(`${basePath}../../../../${
this.manifest.metadata.node.dynamicElementLoader[tagName]
}`)
.then(response => {
//console.log(tagName + ' dynamic import');
})
.catch(error => {
/* Error handling */
console.log(error);
});
}
}
} else if (window.WCAutoload) {
setTimeout(() => {
window.WCAutoload.process();
}, 0);
}
As you can see we have two methods of loading tags. The first method is our original one, which will be phased out (it's for older sites prior to our unbundled routine, which we expect to be able to remove without issue in the next month or so). The routine used is interesting though because...
- It loads the content / knows what HTML was just inserted
- Forms an array of tags that ARE NOT HTML primatives (p,bold,etc)
- Those tags that are then NOT defined, it looks in the registry
- If they are in the registry, it loads them An interesting note here is that we do not use a MutationObserver to accomplish this. We have in other prototypes, but HAXcms leverages MobX so it's actually able to just wait for the route to change and then reacts to the Promise resolution of the content being loaded / inserted.
The modern routine you can see is 1 function wrapped in a setTimeout
. This is a fun "hack" we leverage all over the place for timing purposes as setTimeout
forces the code to execute in the micro-task after the present one. This allows us to skip the use of MutationObserver again because we know that content has been rendered to the page. Calling the window.WCAutoload.process()
call is what actually checks the DOM for undefined tags and then dynamically hydrates them.
Before you ask in the comments - We're not opposed to MutationObservers, I just didn't need one because we had other ways of getting the same information and those methods HAD to fire anyway so this just saves on execution. We use Observers heavily as you can read in this earlier dev.to post.
Here's a screenshot of what this looks like on WCFactory's website:
As you can see, the UI layer has loaded a long time before the wikipedia-query
tag. There's a large gap in when the JS files have loaded because I clicked between some pages to demonstrate that the tags found in this content only load when called for. This approach, dynamic assessment followed by dynamic hydration, helps us avoid the pitfalls of other front-end "CMS"-lite solutions which are forced to compile all potential JS assets into a single blob and deliver them regardless of IF those things are used on the page (not naming names...).
Wrap up
I hope this thread of posts was enlightening or at least somewhat interesting to see how a team is approaching entirely web component driven application development (ok, we use MobX for store management and LOVE it).
Areas for improvement in the future that we are aware of and, as a matter of simplifying language, imply that our stuff will "magically" get faster with no changes in downstream applications:
- We compressed the
build.js
entrypoint last month as well as split thebuild-legacy.js
logic out from the normal file. This means that our entryway in every application sped up from a 3kb entryway to a sub 1kb. No application changes, yet all applications loaded faster. - If we make a partial bundle of things we KNOW are used everywhere (LitElement, some utility functions) into our own sub-class which we then compressed / DID actually bundle just as an entryway, we might be able to speed EVERYTHING up from there. Still considering this but was suggested by Alex Russel in a twitter thread.
- As we improve our compression and TTYL logic, any application loading via our approach will appear to be faster with no effort on the end-user's end.
- We have taken months and years to build some of these elements. Before you say "this assumes the APIs for elements don't change" We agree. That's a large assumption, but one that we can make because our core properties (of which there are already 100s in production) ALSO depend on the elements as being individual APIs. We also have taken a stance that if we need flexibility in
video-player
we'll always ADD to an API of an element but never take away OR we'll just make a new tag likescroll-based-video
which would extend theVideoPlayer
class.
This approach has radically transformed our ability to scale our capabilities without scaling up our team. We're able to read audiences that previously would need to understand the intricacies of build routines and web components in general, so that "HTML people" can just copy and paste two lines of code and start leveraging our team's work. This is critical in a large institution that is made of small teams or those ambitiously trying to change the world one brick at a time.
Get involved
The world isn't changed overnight, by one person. It's lots of people contributing efforts day by day in a consistent direction. Will you join our effort to change the tide? To decentralize powerful monoculture that built financial models on addiction to rage so that we can have better, more vibrant conversations regardless of how or where we have them?
Posted on July 22, 2020
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.