Thursday, May 23, 2024
HomeProgrammingAn Method to Lazy Loading Customized Parts | CSS-Tips

An Method to Lazy Loading Customized Parts | CSS-Tips


We’re followers of Customized Parts round right here. Their design makes them significantly amenable to lazy loading, which generally is a boon for efficiency.

Impressed by a colleague’s experiments, I lately set about writing a easy auto-loader: At any time when a {custom} aspect seems within the DOM, we wanna load the corresponding implementation if it’s not out there but. The browser then takes care of upgrading such components from there on out.

Chances are high you gained’t really want all this; there’s often a less complicated strategy. Used intentionally, the strategies proven right here would possibly nonetheless be a helpful addition to your toolset.

For consistency, we would like our auto-loader to be a {custom} aspect as effectively — which additionally means we are able to simply configure it through HTML. However first, let’s establish these unresolved {custom} components, step-by-step:

class AutoLoader extends HTMLElement {
  connectedCallback() {
    let scope = this.parentNode;
    this.uncover(scope);
  }
}
customElements.outline("ce-autoloader", AutoLoader);

Assuming we’ve loaded this module up-front (utilizing async is good), we are able to drop a <ce-autoloader> aspect into the <physique> of our doc. That may instantly begin the invention course of for all baby components of <physique>, which now constitutes our root aspect. We may restrict discovery to a subtree of our doc by including <ce-autoloader> to the respective container aspect as a substitute — certainly, we would even have a number of situations for various subtrees.

In fact, we nonetheless need to implement that uncover technique (as a part of the AutoLoader class above):

uncover(scope) {
  let candidates = [scope, ...scope.querySelectorAll("*")];
  for(let el of candidates) {
    let tag = el.localName;
    if(tag.consists of("-") && !customElements.get(tag)) {
      this.load(tag);
    }
  }
}

Right here we test our root aspect together with each single descendant (*). If it’s a {custom} aspect — as indicated by hyphenated tags — however not but upgraded, we’ll try and load the corresponding definition. Querying the DOM that means could be costly, so we needs to be a bit of cautious. We will alleviate load on the primary thread by deferring this work:

connectedCallback() {
  let scope = this.parentNode;
  requestIdleCallback(() => {
    this.uncover(scope);
  });
}

requestIdleCallback is just not universally supported but, however we are able to use requestAnimationFrame as a fallback:

let defer = window.requestIdleCallback || requestAnimationFrame;

class AutoLoader extends HTMLElement {
  connectedCallback() {
    let scope = this.parentNode;
    defer(() => {
      this.uncover(scope);
    });
  }
  // ...
}

Now we are able to transfer on to implementing the lacking load technique to dynamically inject a <script> aspect:

load(tag) {
  let el = doc.createElement("script");
  let res = new Promise((resolve, reject) => {
    el.addEventListener("load", ev => {
      resolve(null);
    });
    el.addEventListener("error", ev => {
      reject(new Error("didn't find custom-element definition"));
    });
  });
  el.src = this.elementURL(tag);
  doc.head.appendChild(el);
  return res;
}

elementURL(tag) {
  return `${this.rootDir}/${tag}.js`;
}

Observe the hard-coded conference in elementURL. The src attribute’s URL assumes there’s a listing the place all {custom} aspect definitions reside (e.g. <my-widget>/elements/my-widget.js). We may give you extra elaborate methods, however that is adequate for our functions. Relegating this URL to a separate technique permits for project-specific subclassing when wanted:

class FancyLoader extends AutoLoader {
  elementURL(tag) {
    // fancy logic
  }
}

Both means, word that we’re counting on this.rootDir. That is the place the aforementioned configurability is available in. Let’s add a corresponding getter:

get rootDir() {
  let uri = this.getAttribute("root-dir");
  if(!uri) {
    throw new Error("can not auto-load {custom} components: lacking `root-dir`");
  }
  if(uri.endsWith("https://css-tricks.com/")) { // take away trailing slash
    return uri.substring(0, uri.size - 1);
  }
  return uri;
}

You could be pondering of observedAttributes now, however that doesn’t actually make issues simpler. Plus updating root-dir at runtime looks as if one thing we’re by no means going to wish.

Now we are able to — and should — configure our components listing: <ce-autoloader root-dir="/elements">.

With this, our auto-loader can do its job. Besides it solely works as soon as, for components that exist already when the auto-loader is initialized. We’ll most likely wish to account for dynamically added components as effectively. That’s the place MutationObserver comes into play:

connectedCallback() {
  let scope = this.parentNode;
  defer(() => {
    this.uncover(scope);
  });
  let observer = this._observer = new MutationObserver(mutations => {
    for(let { addedNodes } of mutations) {
      for(let node of addedNodes) {
        defer(() => {
          this.uncover(node);
        });
      }
    }
  });
  observer.observe(scope, { subtree: true, childList: true });
}

disconnectedCallback() {
  this._observer.disconnect();
}

This fashion, the browser notifies us at any time when a brand new aspect seems within the DOM — or moderately, our respective subtree — which we then use to restart the invention course of. (You would possibly argue we’re re-inventing {custom} components right here, and also you’d be form of right.)

Our auto-loader is now absolutely purposeful. Future enhancements would possibly look into potential race circumstances and examine optimizations. However likelihood is that is adequate for many eventualities. Let me know within the feedback in case you have a unique strategy and we are able to evaluate notes!

RELATED ARTICLES

LEAVE A REPLY

Please enter your comment!
Please enter your name here

Most Popular

Recent Comments