{"id":376991,"date":"2023-02-13T07:10:41","date_gmt":"2023-02-13T15:10:41","guid":{"rendered":"https:\/\/css-tricks.com\/?p=376991"},"modified":"2023-02-13T07:10:44","modified_gmt":"2023-02-13T15:10:44","slug":"an-approach-to-lazy-loading-custom-elements","status":"publish","type":"post","link":"https:\/\/css-tricks.com\/an-approach-to-lazy-loading-custom-elements\/","title":{"rendered":"An Approach to Lazy Loading Custom Elements"},"content":{"rendered":"\n

We’re fans of Custom Elements<\/a> around here. Their design makes them particularly amenable to lazy loading<\/a>, which can be a boon for performance.<\/p>\n\n\n\n

Inspired by a colleague’s<\/a> 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.<\/p>\n\n\n\n\n\n\n\n

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.<\/p>\n\n\n\n

For consistency, we want our auto-loader to be a custom element as well \u2014 which also means we can easily configure it via HTML. But first, let’s identify those unresolved custom elements, step by step:<\/p>\n\n\n\n

class AutoLoader extends HTMLElement {\n  connectedCallback() {\n    let scope = this.parentNode;\n    this.discover(scope);\n  }\n}\ncustomElements.define(\"ce-autoloader\", AutoLoader);<\/code><\/pre>\n\n\n\n

Assuming we’ve loaded this module up-front (using async<\/code><\/a> is ideal), we can drop a <ce-autoloader><\/code> element into the <body><\/code> of our document. That will immediately start the discovery process for all child elements of <body><\/code>, which now constitutes our root element. We could limit discovery to a subtree of our document by adding <ce-autoloader><\/code> to the respective container element instead \u2014 indeed, we might even have multiple instances for different subtrees.<\/p>\n\n\n\n

Of course, we still have to implement that discover<\/code> method (as part of the AutoLoader<\/code> class above):<\/p>\n\n\n\n

discover(scope) {\n  let candidates = [scope, ...scope.querySelectorAll(\"*\")];\n  for(let el of candidates) {\n    let tag = el.localName;\n    if(tag.includes(\"-\") && !customElements.get(tag)) {\n      this.load(tag);\n    }\n  }\n}<\/code><\/pre>\n\n\n\n

Here we check our root element along with every single descendant (*<\/code>). If it’s a custom element \u2014 as indicated by hyphenated tags \u2014 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:<\/p>\n\n\n\n

connectedCallback() {\n  let scope = this.parentNode;\n  requestIdleCallback(() => {\n    this.discover(scope);\n  });\n}<\/code><\/pre>\n\n\n\n

requestIdleCallback<\/code><\/a> is not universally supported yet, but we can use requestAnimationFrame<\/code><\/a> as a fallback:<\/p>\n\n\n\n

let defer = window.requestIdleCallback || requestAnimationFrame;\n\nclass AutoLoader extends HTMLElement {\n  connectedCallback() {\n    let scope = this.parentNode;\n    defer(() => {\n      this.discover(scope);\n    });\n  }\n  \/\/ ...\n}<\/code><\/pre>\n\n\n\n

Now we can move on to implementing the missing load<\/code> method to dynamically inject a <script><\/code> element:<\/p>\n\n\n\n

load(tag) {\n  let el = document.createElement(\"script\");\n  let res = new Promise((resolve, reject) => {\n    el.addEventListener(\"load\", ev => {\n      resolve(null);\n    });\n    el.addEventListener(\"error\", ev => {\n      reject(new Error(\"failed to locate custom-element definition\"));\n    });\n  });\n  el.src = this.elementURL(tag);\n  document.head.appendChild(el);\n  return res;\n}\n\nelementURL(tag) {\n  return `${this.rootDir}\/${tag}.js`;\n}<\/code><\/pre>\n\n\n\n

Note the hard-coded convention in elementURL<\/code>. The src<\/code> attribute’s URL assumes there’s a directory where all custom element definitions reside (e.g. <my-widget><\/code> \u2192 \/components\/my-widget.js<\/code>). We could come up with more elaborate strategies, but this is good enough for our purposes. Relegating this URL to a separate method allows for project-specific subclassing when needed:<\/p>\n\n\n\n

class FancyLoader extends AutoLoader {\n  elementURL(tag) {\n    \/\/ fancy logic\n  }\n}<\/code><\/pre>\n\n\n\n

Either way, note that we’re relying on this.rootDir<\/code>. This is where the aforementioned configurability comes in. Let’s add a corresponding getter:<\/p>\n\n\n\n

get rootDir() {\n  let uri = this.getAttribute(\"root-dir\");\n  if(!uri) {\n    throw new Error(\"cannot auto-load custom elements: missing `root-dir`\");\n  }\n  if(uri.endsWith(\"\/\")) { \/\/ remove trailing slash\n    return uri.substring(0, uri.length - 1);\n  }\n  return uri;\n}<\/code><\/pre>\n\n\n\n

You might be thinking of observedAttributes<\/code> now, but that doesn’t really make things easier. Plus updating root-dir<\/code> at runtime seems like something we\u2019re never going to need.<\/p>\n\n\n\n

Now we can \u2014 and must \u2014 configure our elements directory: <ce-autoloader root-dir=\"\/components\"><\/code>.<\/p>\n\n\n\n

With this, our auto-loader can do its job. Except it only works once, for elements that already exist when the auto-loader is initialized. We’ll probably want to account for dynamically added elements as well. That’s where MutationObserver<\/code><\/a> comes into play:<\/p>\n\n\n\n

connectedCallback() {\n  let scope = this.parentNode;\n  defer(() => {\n    this.discover(scope);\n  });\n  let observer = this._observer = new MutationObserver(mutations => {\n    for(let { addedNodes } of mutations) {\n      for(let node of addedNodes) {\n        defer(() => {\n          this.discover(node);\n        });\n      }\n    }\n  });\n  observer.observe(scope, { subtree: true, childList: true });\n}\n\ndisconnectedCallback() {\n  this._observer.disconnect();\n}<\/code><\/pre>\n\n\n\n

This way, the browser notifies us whenever a new element appears in the DOM \u2014 or rather, our respective subtree \u2014 which we then use to restart the discovery process. (You might argue we\u2019re re-inventing custom elements here, and you\u2019d be kind of correct.)<\/p>\n\n\n\n

Our auto-loader is now fully functional. Future enhancements might look into potential race conditions and investigate optimizations. But chances are this is good enough for most scenarios. Let me know in the comments if you have a different approach and we can compare notes!<\/p>\n","protected":false},"excerpt":{"rendered":"

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. […]<\/p>\n","protected":false},"author":288729,"featured_media":376994,"comment_status":"open","ping_status":"closed","sticky":false,"template":"","format":"standard","meta":{"_bbp_topic_count":0,"_bbp_reply_count":0,"_bbp_total_topic_count":0,"_bbp_total_reply_count":0,"_bbp_voice_count":0,"_bbp_anonymous_reply_count":0,"_bbp_topic_count_hidden":0,"_bbp_reply_count_hidden":0,"_bbp_forum_subforum_count":0,"sig_custom_text":"","sig_image_type":"featured-image","sig_custom_image":0,"sig_is_disabled":false,"inline_featured_image":false,"c2c_always_allow_admin_comments":false,"footnotes":"","jetpack_publicize_message":"An Approach to Lazy Loading Custom Elements by Frederik Dohr","jetpack_is_tweetstorm":false,"jetpack_publicize_feature_enabled":true,"jetpack_social_post_already_shared":false,"jetpack_social_options":[]},"categories":[4],"tags":[1536,908,803],"jetpack_publicize_connections":[],"acf":[],"jetpack_featured_media_url":"https:\/\/i0.wp.com\/css-tricks.com\/wp-content\/uploads\/2023\/02\/wev-components-lazy.png?fit=1200%2C600&ssl=1","jetpack-related-posts":[{"id":260267,"url":"https:\/\/css-tricks.com\/lozad-js-performant-lazy-loading-images\/","url_meta":{"origin":376991,"position":0},"title":"Lozad.js: Performant Lazy Loading of Images","date":"September 26, 2017","format":false,"excerpt":"There are a few different \"traditional\" ways of lazy loading of images. They all require JavaScript needing to figure out if an image is currently visible within the browser's viewport or not. Traditional approaches might be: Listening to scroll and resize events on the window Using a timer like setInterval\u2026","rel":"","context":"In "Article"","img":{"alt_text":"","src":"https:\/\/i0.wp.com\/css-tricks.com\/wp-content\/uploads\/2017\/09\/s_29E8C45E02B8336138FF9D42F6FED1CE28964941DD8BFD3FD5E751E96FE0C633_1504810907674_lozad-banner.png?fit=1200%2C457&ssl=1&resize=350%2C200","width":350,"height":200},"classes":[]},{"id":291777,"url":"https:\/\/css-tricks.com\/tips-for-rolling-your-own-lazy-loading\/","url_meta":{"origin":376991,"position":1},"title":"Tips for rolling your own lazy loading","date":"July 1, 2019","format":false,"excerpt":"You may have heard (or even issued the call) that \u201cwe can just use lazy loading!\u201d when looking for a way to slim down a particularly heavy web page. Lazy loading is a popular technique for gradually requesting images as they come into view, rather than all at once after\u2026","rel":"","context":"In "Article"","img":{"alt_text":"","src":"https:\/\/i0.wp.com\/css-tricks.com\/wp-content\/uploads\/2018\/08\/lazyload.png?fit=1200%2C600&ssl=1&resize=350%2C200","width":350,"height":200},"classes":[]},{"id":294405,"url":"https:\/\/css-tricks.com\/weekly-platform-news-html-loading-attribute-the-main-aria-specifications-and-moving-from-iframe-to-shadow-dom\/","url_meta":{"origin":376991,"position":2},"title":"Weekly Platform News: HTML Loading Attribute, the Main ARIA Specifications, and Moving from iFrame to Shadow DOM","date":"August 15, 2019","format":false,"excerpt":"In this week's roundup of platform news, Chrome introduces a new attribute for loading, accessibility specifications for web developers, and the BBC moves visualizations to the Shadow DOM. Chrome ships the loading attribute The HTML loading attribute for lazy-loading images and iframes is now supported in Chrome. You can add\u2026","rel":"","context":"In "Weekly Platform News"","img":{"alt_text":"","src":"https:\/\/i0.wp.com\/css-tricks.com\/wp-content\/uploads\/2019\/08\/wpn-190814.png?fit=1200%2C600&ssl=1&resize=350%2C200","width":350,"height":200},"classes":[]},{"id":277081,"url":"https:\/\/css-tricks.com\/lazy-loading-images-with-vue-js-directives-and-intersection-observer\/","url_meta":{"origin":376991,"position":3},"title":"Lazy Loading Images with Vue.js Directives and Intersection Observer","date":"October 15, 2018","format":false,"excerpt":"When I think about web performance, the first thing that comes to my mind is how images are generally the last elements that appear on a page. Today, images can be a major issue when it comes to performance, which is unfortunate since the speed a website loads has a\u2026","rel":"","context":"In "Article"","img":{"alt_text":"","src":"https:\/\/i0.wp.com\/css-tricks.com\/wp-content\/uploads\/2018\/09\/lazy-load-image.png?fit=1200%2C600&ssl=1&resize=350%2C200","width":350,"height":200},"classes":[]},{"id":279419,"url":"https:\/\/css-tricks.com\/preventing-content-reflow-from-lazy-loaded-images\/","url_meta":{"origin":376991,"position":4},"title":"Preventing Content Reflow From Lazy-Loaded Images","date":"November 29, 2018","format":false,"excerpt":"You know the concept of lazy loading images. It prevents the browser from loading images until those images are in (or nearly in) the browser's viewport. There are a plethora of JavaScript-based lazy loading solutions. GitHub has over 3,400 different lazy load repos, and those are just the ones with\u2026","rel":"","context":"In "Article"","img":{"alt_text":"","src":"https:\/\/i0.wp.com\/css-tricks.com\/wp-content\/uploads\/2018\/11\/weightlift-image.png?fit=1200%2C600&ssl=1&resize=350%2C200","width":350,"height":200},"classes":[]},{"id":287494,"url":"https:\/\/css-tricks.com\/a-deep-dive-into-native-lazy-loading-for-images-and-frames\/","url_meta":{"origin":376991,"position":5},"title":"A Deep Dive into Native Lazy-Loading for Images and Frames","date":"May 15, 2019","format":false,"excerpt":"Today's websites are packed with heavy media assets like images and videos. Images make up around 50% of an average website's traffic. Many of them, however, are never shown to a user because they're placed way below the fold. What\u2019s this thing about images being lazy, you ask? Lazy-loading is\u2026","rel":"","context":"In "Article"","img":{"alt_text":"","src":"https:\/\/i0.wp.com\/css-tricks.com\/wp-content\/uploads\/2019\/05\/diving-board-1.png?fit=1200%2C600&ssl=1&resize=350%2C200","width":350,"height":200},"classes":[]}],"featured_media_src_url":"https:\/\/i0.wp.com\/css-tricks.com\/wp-content\/uploads\/2023\/02\/wev-components-lazy.png?fit=1024%2C512&ssl=1","_links":{"self":[{"href":"https:\/\/css-tricks.com\/wp-json\/wp\/v2\/posts\/376991"}],"collection":[{"href":"https:\/\/css-tricks.com\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/css-tricks.com\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/css-tricks.com\/wp-json\/wp\/v2\/users\/288729"}],"replies":[{"embeddable":true,"href":"https:\/\/css-tricks.com\/wp-json\/wp\/v2\/comments?post=376991"}],"version-history":[{"count":10,"href":"https:\/\/css-tricks.com\/wp-json\/wp\/v2\/posts\/376991\/revisions"}],"predecessor-version":[{"id":377192,"href":"https:\/\/css-tricks.com\/wp-json\/wp\/v2\/posts\/376991\/revisions\/377192"}],"wp:featuredmedia":[{"embeddable":true,"href":"https:\/\/css-tricks.com\/wp-json\/wp\/v2\/media\/376994"}],"wp:attachment":[{"href":"https:\/\/css-tricks.com\/wp-json\/wp\/v2\/media?parent=376991"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/css-tricks.com\/wp-json\/wp\/v2\/categories?post=376991"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/css-tricks.com\/wp-json\/wp\/v2\/tags?post=376991"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}