logo ce-autoloader

Load web components on demand
if and when they're needed.

About

You need a custom <select> menu. Suddenly, you've installed 500MB of node_modules, wrapped your site inside a russian doll of <App><Provider><Router tags, and then wait for 1MB of JavaScript to run before the user can even scroll. ๐Ÿ˜–

When you try to fix it with SSR, you're now married to the framework and the "Hydration Mismatch" horror movie โ€” where the server and client start arguing about what a Date object looks like. Now you're paying for a beefy server just to stringify JSON into HTML, while losing your mind over cache-control headers that are never quite right. Content is stale, CPU is 100%, and your wallet is crying.

I just want static pages with a few interactive components.
If a component isnโ€™t used on the page, it shouldnโ€™t be downloaded.
The fact that this is still hard is kind of absurd.

ce-autoloader is a lightweight library born out of this frustration. It brings Islands Architecture to any site, with any framework.

The special sauce? A shared component catalog, lazy-loading components by default and CSS Animation/View Transitions API to animate them into place.

Fast sites, minimal JavaScript, without over-engineering. ๐Ÿ˜ฎโ€๐Ÿ’จ

Get started

It's just 3 steps. No build tools required (but recommended).

  1. Install via npm or CDN
  2. Define your catalog
  3. Sprinkle your web components on the page
npm install ce-autoloader
import CERegistry from 'ce-autoloader';

const registry = new CERegistry({
	catalog: {
		'my-component': () => import('./my-component.js')
	}
});

// Start the magic ๐Ÿช„
registry.discover();
<p>Hello <my-component on="visible">World</my-component></p>

Documentation

Configuration

When creating a new CERegistry, you can pass an options object to customize its behavior.

Option Type Default Description
catalog Record<string, string | function> Required A mapping of tag names to their respective loader functions or URLs.
root HTMLElement document.body The root element where the loader will search for custom elements.
live boolean true Automatically watches the DOM for new custom elements to upgrade.
transition boolean true Enables the View Transitions API for smooth element upgrades.
directives string[] ['eager', 'visible', 'click'] A list of supported loading triggers.
defaultDirective string 'visible' The default loading strategy used when the on attribute is missing.
fallback CustomElementConstructor undefined A custom element class to use if a component fails to load.

Activation Triggers (Loading Strategies)

The on attribute defines the strategy for when a component is activated. By default, components use the visible strategy to maximize initial performance.

visible (Default)
Lazy-loading via Intersection Observer. Components are downloaded and defined only when they enter the viewport. Perfect for content "below the fold" to keep initial page weight low.
click
Hydrate on demand. The module is fetched when a user clicks or touches the element. Ideal for heavy UI widgets like maps, complex editors, or modal content.
eager
Immediate activation. Downloads the module as soon as the loader discovery starts. Use this for critical "above the fold" components like navigation headers or hero sections.
*custom*
Manual/Programmatic mode. You can specify any custom string (e.g., on="lazy-load") and trigger it manually via registry.upgrade('lazy-load').
Nord-banner was loaded. Click me to load ๐ŸŽ‰ Click me to throw confetti ๐ŸŽ‰

Dynamic Resolvers (Wildcard Loaders)

Don't want to register 50 components one by one? Pode crer, we got you. Dynamic Resolvers (or Wildcard Loaders) allow you to register a function that "resolves" any tag matching a specific pattern.

In this example, we registered a resolver for nord-*. The browser sees <nord-qrcode>, and our resolver instantly fetches the Correct module from the CDN.

  • No Giant Bundles: Only the components on the page are downloaded.
  • Pure HTML Workflow: Add a tag, and it just works.
  • Industry Standard: Same pattern used by Vite and Auto-import plugins.
Leve e rรกpido. (Light and fast)

Here is an example of a Wildcard Resolver for the Nord design system:

/**
 * Pattern-based Resolver for Nord Health components
 */
"nord-*": async (full_name) => {
    // 1. Extract the specific component name (e.g., 'qrcode' from 'nord-qrcode')
    const [, namespace, name] = full_name.match(/^([a-z]+)-(.*)/);

    // 2. Resolve the module URL (dynamic import)
    const module = await import(/* @vite-ignore */ `https://esm.sh/@nordhealth/components/lib/${capitalize(name)}.js`);

    // 3. Define the component if not already registered
    if (!customElements.get(full_name)) {
        customElements.define(full_name, module.default);
    }
    return module
},

Entrance Lifecycle & Transitions

Web components shouldn't just "pop" into existence. ce-autoloader provides a robust lifecycle state machine that allows you to style components as they transition from placeholders to interactive elements, effectively eliminating Cumulative Layout Shift (CLS).

The Discovery Lifecycle

Every component moves through three distinct states, that you can style using CSS.

:not(:defined)
Placeholder State. The custom element is in the DOM but the browser doesn't know its behavior yet. Use this to set min-height or aspect-ratio to reserve space on the page.
[ce="loading"]
Activation State. Triggered when the library begins fetching the module. This is the perfect time to show a Skeleton Loader or a progress indicator.
[ce="defined"]
interactive State. The component is defined and upgraded. Use this to trigger a final "fade-in" or entrance animation.

Besides the lifecycle states, view transitions are supported out of the box.

You can use the view-transition-name attribute to include a view transition


๐ŸŒœ Phases of the moon ๐ŸŒ›

Native Telemetry & Insights

Performance is built into the core. ce-autoloader uses the native User Timing API to provide granular metrics for every stage of your components' lifecycle. This allows you to quantify the exact impact of your loading strategies on Core Web Vitals.

Observability has native integration into performance devtools

High-resolution markers

Granular metrics for every stage of the lifecycle, visible in the Performance Tab.

  • load:tag-name: Fetch duration.
  • define:tag-name: JS registration time.
  • transition: Animation duration.

Performance Measure

// Export all load times
performance.getEntriesByType('measure')
    .filter(e => e.name.startsWith('load:'));

FAQ (Perguntas Frequentes)

Why web components?

Web components with light-dom&css variables are the GOAT! They are the only way to reuse components across frameworks, and since they're part of HTML spec, they'll still work in 20 years.

Aren't you tired of recreating the same components over and over again every 5 years with the new shiny framework? Let's stop this madness!

โš›๏ธ How to use this with react/vue/svelte/etรง?

Vue and Svelte already export web-components out of the box!

Only React is purposefully missing this feature, so you need to use a library like remount or react-to-web-component.

import r2wc from "@r2wc/react-to-web-component"

const Greeter = ({ name }) => {
	return <div>Hello, {name}!</div>
}

export default r2wc(Greeter, {
	props: {
		name: "string",
	}
});

					

๐Ÿš€ Wait, isn't this like Astro?

Yes and No. Both use the "Island Architecture" concept to reduce JS bloat, but they are :

  • Astro (Server-First): Renders at build time or at server. Great for new apps.
  • ce-autoloader (Client-First): Runs at runtime. Great for adding islands to existing Python, Ruby, Elixir or static HTML pages.

๐Ÿš€ What's the performance like?

Glad you asked! Core Web Vitals with ce-autoloader matches or exceeds SSR frameworks like Next.js. Don't believe me? This is the score of the page you're reading now:

Core Web Vitals