Lazyloading Web Components

They say good developers are lazy. A tedious process is no match for a lazy developer. Below is an experiment in laziness. It’s a proof of concept for using Web Components without having to manually import each one.

I’m not sure exactly what to call it. It grazes the definitions of lazyloading, autoloading, and dependency injection, so I went with the laziest term.

Let’s set the scene, and find out if it’s a worthwhile developer convenience, or ease-of-use gone mad…

# Scene 1: Maximize Simplicity

You are the author of “Wootstrap”, a Bootstrap-like library of useful components like navigation, buttons, and alerts. Also like Bootstrap, your components are distributed as HTML snippets for users to copy, paste, and customize.

To improve ease of use, you encapsulate all that HTML into a custom element:

<ws-panel heading="I am a ws-panel">Some words.</ws-panel>

The essence of simplicity! One more thing, browsers don’t know what <ws-panel> is, so your users must import it:

<link rel="import" href="../bower_components/wootstrap/components/ws-panel.html">

One line, no big deal. But as your users add and remove components from their pages, managing imports becomes a finger-aching chore.

# Scene 2: Minimize Finger-Ache

Wouldn’t it be cushy if the components were imported when you use them, automatically?

# Live demo

# Demo code

<html>
    <head>
        <script defer src="src/webcomponents.min.js"></script>
        <link rel="import" href="src/lazy-load/lazy-load.html">
    </head>
    <body>
        <lazy-load>
            <ws-panel>I am a ws-panel.</ws-panel>
            <ws-btn>I am ws-button</ws-btn>
            <ws-alert>I am a ws-alert</ws-alert>
        </lazy-load>
    </body>
</html>

The demo only imports lazy-load, which encapsulates the importing of the rest of the elements so you don’t have to specify them by hand. Let’s look at how it works.

# Scene 3: Behind the Scenes

To lazyload, we need a list of all our component names and where to find them. I created a small “registry” object for the demo. In the real-world, this object could be auto-generated by the component library’s build system, or by the build system of the user’s app, including all available components.

var LazyComponents = {
  names: "ws-panel,ws-btn,ws-alert",
  paths: {
    "ws-panel": "./src/ws-components/ws-panel/ws-panel.html",
    "ws-btn": "./src/ws-components/ws-btn/ws-btn.html",
    "ws-alert": "./src/ws-components/ws-alert/ws-alert.html"
  }
};

First we have names, a comma-separated list of component names which doubles as a selector. It is passed into querySelectorAll() to find any occurrences of our elements on the page. Second is paths, a mapping from each component’s name to its HTML template file.

# The lazy-load element

Here’s the Polymer definition for the lazy-load element:

Polymer({

    is: 'lazy-load',

    attached: function lazyLoadAttached() {
        this.lazyLoad();
    },

    lazyLoad: function lazyLoad() {
        var els = this.querySelectorAll(LazyComponents.names);
        for (var i = 0; i &lt; els.length; ++i) {
            var name = els[i].nodeName.toLowerCase();
            this.importHref(LazyComponents.paths[name]);
        }
    },

});

When lazy-load is attached to the DOM, it finds any children in the names list and imports their HTML templates (using Polymer’s handy importHref() function).

Now any element in the registry object can be used without an explicit import.

# Tradeoffs

There are some things that may outweigh the convenience.

  • Unvulcanized - Lazy loading is the opposite of Vulcanize. Vulcanize can still be used, but lazyloaded components won’t be inlined.
  • Delayed loading - Explicit imports begin downloading before lazyloaded imports, since they don’t have to wait for lazy-load to be attached to the DOM.
  • Paths must be absolute - Imports typically use relative paths, but lazyloaded paths are defined in only one spot, not per-page, so they must be absolute, or relative to lazy-load.html.
  • Late to the party? - Any custom elements added to the page after lazyLoad() has run won’t be imported. That didn’t seem necessary for a proof of concept, but running lazyLoad() on each state change would be an easy solution.
  • HTTP/2 Server Push? - I haven’t tested this, but I suspect lazyloaded components won’t benefit from HTTP/2 Server Push.

I’m not sure yet which camp lazyloading will fall into: best practice, or antipattern. My hope is that developer tooling will be written that injects imports as you code, rather than at runtime.

Thanks to Kyle Buchanan for pointers and corrections!