[w3c/webcomponents] Lazy Custom Element Definitions (#782)

As mentioned in #716 and discussed at the 2018 Tokyo f2f, lazy definitions would be a useful addition to the specs.

## What

A lazy definition would take a tag name and an async function that returns the class to be defined lazily. When the tag is first encountered the browser will invoke the function:

```js
customElements.lazyDefine('my-element', async () => (await import('./my-element.js')).MyElement);
```

For ergonomics, we may want to support a default export as the element class:

```js
customElements.lazyDefine('my-element', () => import('./my-element.js')));
```

## Why

Dynamic imports are an important way to load code at the point in time when it's actually needed, and not cause unneeded cost and latency by loading earlier than that. Modern build tools and bundlers are able to bundle and preserve the dynamic import boundaries as code-split points. Right now this works great for situations where application code can explicitly know that it needs the new code, so we typically see it used when there's a user action or around navigation with route-based loading and therefore route-based code-splitting.

Maybe we'd see code like this to navigate:

```js
if (page === 'product') {
  const ProductPage = await import('./product-page.js');
  shell.appendChild(new ProductPage());
}
```

This is fine as long as the product-page bundle isn't itself too large, and/or most of the components in the bundle are used on every product page. But in some cases a page/route may be far too coarse grained to get effective splitting. ie, any one product page may use only a fraction of the potential features of a product page.

The recent trend is to take advantage of tooling support for dynamic import() to do component-base code-splitting.

React recently added a `lazy()` component wrapper:

```js
const OtherComponent = React.lazy(() => import('./OtherComponent'));

function MyComponent() {
  return (
    <div>
      <OtherComponent />
    </div>
  );
}
```

When `OtherComponent` is _used_, its implementation will be fetched.

In the Web Components ecosystem, Stencil builds lazy-loaded components by default. They generate a stub that is registered and loads the implementation on first creation.

On the Polymer team we have experimented with this as well with something called "SplitElement": https://github.com/PolymerLabs/split-element

These approaches work by registering a stub class with lifecycle callbacks and `observedAttributed` known at definition time.

The problem with the stub approach is that each element must be written to be lazy loaded, you can't just lazy load an arbitrary component. Then the implementation is non trivial as the stub has to perform something akin to upgrading between itself and the implementation class. If you want to support constructors in the implementation class, you also have to do the "constructor call trick".

It would be much nicer and more general if the platform supported this directly.

## Details

### defineLazy:

`CustomElementRegistry#defineLazy(tagName: string, loader: () => Promise<CustomElementDefinition>)`

### Loading

When a lazy-defined element is created, it's associated loader function is called.

### element-definition-loading Event

The elements created before the definition is loaded also fire an event: `element-definition-loading`. This allows generic code above the lazy elements in the tree to display user affordances while code is loading, like a spinner.

The event should carry a Promise that resolves when the element is upgraded. This Promise can be the same Promise returned by `whenDefined(tagName)`.

## Alternate Solutions

Another approach is to allow for a generic callback for potentially custom, but undefined, elements. This callback could then load and register the element definition. It's more general, but probably less ergonomic.

## Polyfilling

This feature can be polyfilled by using a MutationObserver to watch for lazy element instances. In order to work in ShadowRoots, the polyfill will have to patch `attachShadow`.

-- 
You are receiving this because you are subscribed to this thread.
Reply to this email directly or view it on GitHub:
https://github.com/w3c/webcomponents/issues/782

Received on Tuesday, 15 January 2019 18:46:43 UTC