Skip to content

Async Component Loading ​

Load web component HTML files on demand by fetching their content, parsing it into a DocumentFragment, and injecting templates, styles, and scripts into the document. This reduces initial page weight by deferring component loading until needed.

When to use ​

Use async loading to defer component initialization until the component is needed. It works well for multi-page apps where different pages use different components and for prototyping without a build step — just fetch and inject.

The pattern ​

Loading a component ​

Import loadComponent and pass the URL of a component HTML file. It fetches, parses, and injects the component's template, styles, and script into the document so the custom element is available immediately.

html
<script type="module">
  import { loadComponent } from "./wck.js";
  await loadComponent("./my-counter.component.html");
</script>

<my-counter></my-counter>

Component file structure ​

Each component bundles its template, styles, and class definition in a single .html file.

html
<!-- my-counter.component.html -->
<template name="my-counter">
  <div class="counter">
    <button on:click="data.count--">-</button>
    <span>{{ data.count }}</span>
    <button on:click="data.count++">+</button>
  </div>
</template>

<style>
  .counter {
    display: flex;
    gap: 1rem;
    align-items: center;
  }
</style>

<script type="module">
  import {
    bindTemplate, reactive,
  } from "@chriscalo/web-component-kit";
  
  class MyCounter extends HTMLElement {
    connectedCallback() {
      this.data = reactive({ count: 0 });
      const template = document.querySelector(
        `template[name="my-counter"]`
      );
      const render = bindTemplate(template, this);
      render();
    }
  }
  
  customElements.define("my-counter", MyCounter);
</script>

The loader implementation ​

loadComponent fetches an HTML file, parses it into a fragment, then distributes its contents: <template> elements go to document.body, <style> elements go to document.head, and <script> elements are re-created as new <script type="module"> nodes because cloned script nodes do not execute.

javascript
export async function fetchText(url) {
  const response = await fetch(url);
  if (response.ok) {
    return response.text();
  }
  throw new Error(
    `Failed to fetch:\n`
    + `  URL:    ${url}\n`
    + `  Status: ${response.status} ${response.statusText}`
  );
}

export function parseToFragment(htmlString) {
  const fragment = new DocumentFragment();
  const temp = document.createElement("div");
  temp.innerHTML = htmlString;
  while (temp.firstChild) {
    fragment.appendChild(temp.firstChild);
  }
  return fragment;
}

export async function loadComponent(url) {
  const html = await fetchText(url);
  const fragment = parseToFragment(html);
  
  // Templates go to body
  for (const tpl of fragment.querySelectorAll("template")) {
    document.body.appendChild(tpl);
  }
  
  // Styles go to head
  for (const style of fragment.querySelectorAll("style")) {
    document.head.appendChild(style);
  }
  
  // Scripts must be re-created to execute
  const pending = [];
  for (const script of fragment.querySelectorAll("script")) {
    if (script.type === "module" || !script.type) {
      const fresh = document.createElement("script");
      fresh.type = "module";
      fresh.textContent = script.textContent;
      pending.push(new Promise((resolve) => {
        fresh.onload = resolve;
        fresh.onerror = resolve;
        document.body.appendChild(fresh);
        // Inline module scripts don't fire load events
        setTimeout(resolve, 0);
      }));
    }
  }
  
  await Promise.all(pending);
}

Trade-offs ​

  • No build step required. Components are plain HTML files loaded at runtime — ideal for prototyping and small apps.
  • Script re-creation. Cloned <script> elements do not execute, so the loader creates fresh elements. This means inline module scripts run but do not fire load events; the setTimeout(resolve, 0) workaround handles that.
  • No shadow DOM. Templates, styles, and scripts inject into the light DOM (body and head), keeping everything in a single flat document scope. This simplifies styling but offers no encapsulation.
  • Network cost per component. Each loadComponent call makes a fetch request. For many components, consider bundling or using HTTP/2 multiplexing.