This article explores a lazy loading method for custom elements to improve web page performance. This method was inspired by colleagues' experiments, and its core idea was to automatically load the corresponding implementation code after custom elements were added to the DOM.
Usually, there is no need for such a complex lazy loading mechanism, but the techniques described in this article are still valuable for specific scenarios.
To maintain consistency, the lazy loader itself is also designed as a custom element for easy configuration through HTML. First, we need to gradually identify unresolved custom elements:
class AutoLoader extends HTMLElement { connectedCallback() { const scope = this.parentNode; this.discover(scope); } } customElements.define("ce-autoloader", AutoLoader);
Assuming we have preloaded this module (ideally using async method), we can add the <ce-autoloader></ce-autoloader>
element to the document. This will immediately start the search process for all child elements that form the root element. By adding <ce-autoloader></ce-autoloader>
to the corresponding container element, you can limit the scope of the lookup to the document's subtree, and even use multiple instances in different subtrees.
Next, we need to implement the discover
method (as part of the above class): AutoLoader
discover(scope) { const candidates = [scope, ...scope.querySelectorAll("*")]; for (const el of candidates) { const tag = el.localName; if (tag.includes("-") && !customElements.get(tag)) { this.load(tag); } } }
connectedCallback() { const scope = this.parentNode; requestIdleCallback(() => { this.discover(scope); }); }
Not all browsers support it, you can use requestIdleCallback
as a backup plan: requestAnimationFrame
const defer = window.requestIdleCallback || requestAnimationFrame; class AutoLoader extends HTMLElement { connectedCallback() { const scope = this.parentNode; defer(() => { this.discover(scope); }); } // ... }
method, dynamically inject the load
elements: <script></script>
load(tag) { const el = document.createElement("script"); const res = new Promise((resolve, reject) => { el.addEventListener("load", () => resolve(null)); el.addEventListener("error", () => reject(new Error("未能找到自定义元素定义"))); }); el.src = this.elementURL(tag); document.head.appendChild(el); return res; } elementURL(tag) { return `${this.rootDir}/${tag}.js`; }
. The URL of the elementURL
attribute assumes that there is a directory containing all custom element definitions (e.g. src
→ <my-widget></my-widget>
). We can adopt more complex strategies, but this is enough for our purposes. Delegate this URL to a separate method to allow project-specific subclassing if needed: /components/my-widget.js
class FancyLoader extends AutoLoader { elementURL(tag) { // 自定义逻辑 } }
. This is the configurability mentioned earlier. Let's add a corresponding getter: this.rootDir
get rootDir() { const uri = this.getAttribute("root-dir"); if (!uri) { throw new Error("无法自动加载自定义元素:缺少`root-dir`属性"); } return uri.endsWith("/") ? uri.substring(0, uri.length - 1) : uri; }
because updating the observedAttributes
property seems unnecessary at runtime. root-dir
. <ce-autoloader root-dir="/components"></ce-autoloader>
comes into play: MutationObserver
class AutoLoader extends HTMLElement { connectedCallback() { const scope = this.parentNode; this.discover(scope); } } customElements.define("ce-autoloader", AutoLoader);
In this way, the browser will notify us when a new element appears in the DOM (more precisely, our corresponding subtree), and we then use it to restart the lookup process.
Our autoloader is now fully usable. Future enhancements may include studying potential competition conditions and optimization. But for most scenarios, that's enough. If you have a different approach, please let me know in the comments and we can communicate with each other!
The above is the detailed content of An Approach to Lazy Loading Custom Elements. For more information, please follow other related articles on the PHP Chinese website!