Explore 30 ways to make a Web Component. Compare coding style, bundle size and performance.

Introduction

The same <my-counter/> Web Component has been written in 30 variants.

Source Code

HTMLElement

Homepage: https://html.spec.whatwg.org/multipage/custom-elements.html

Try in WebComponents.dev
class MyCounter extends HTMLElement {
  constructor() {
    super();
    this.count = 0;

    const style = `
      * {
        font-size: 200%;
      }

      span {
        width: 4rem;
        display: inline-block;
        text-align: center;
      }

      button {
        width: 64px;
        height: 64px;
        border: none;
        border-radius: 10px;
        background-color: seagreen;
        color: white;
      }
    `;

    const html = `
      <button id="dec">-</button>
      <span>${this.count}</span>
      <button id="inc">+</button>
    `;

    this.attachShadow({ mode: 'open' });
    this.shadowRoot.innerHTML = `
    <style>
      ${style}
    </style>
    ${html}
    `;

    this.buttonInc = this.shadowRoot.getElementById('inc');
    this.buttonDec = this.shadowRoot.getElementById('dec');
    this.spanValue = this.shadowRoot.querySelector('span');

    this.inc = this.inc.bind(this);
    this.dec = this.dec.bind(this);
  }

  inc() {
    this.count++;
    this.update();
  }

  dec() {
    this.count--;
    this.update();
  }

  update() {
    this.spanValue.innerText = this.count;
  }

  connectedCallback() {
    this.buttonInc.addEventListener('click', this.inc);
    this.buttonDec.addEventListener('click', this.dec);
  }

  disconnectedCallback() {
    this.buttonInc.removeEventListener('click', this.inc);
    this.buttonDec.removeEventListener('click', this.dec);
  }
}

customElements.define('my-counter', MyCounter);

HTMLElement (w/ Lighterhtml)

The hyperHTML strength & experience without its complexity

Homepage: https://medium.com/@WebReflection/lit-html-vs-hyperhtml-vs-lighterhtml-c084abfe1285

GitHub: https://github.com/WebReflection/lighterhtml

Try in WebComponents.dev
import { html, render } from 'lighterhtml';

class MyCounter extends HTMLElement {
  constructor() {
    super();
    this.count = 0;

    this.attachShadow({ mode: 'open' });

    this.update();
  }

  inc = () => {
    this.count++;
    this.update();
  };

  dec = () => {
    this.count--;
    this.update();
  };

  style() {
    return `
      * {
        font-size: 200%;
      }

      span {
        width: 4rem;
        display: inline-block;
        text-align: center;
      }

      button {
        width: 64px;
        height: 64px;
        border: none;
        border-radius: 10px;
        background-color: seagreen;
        color: white;
      }
    `;
  }

  template() {
    return html`
      <style>
        ${this.style()}
      </style>
      <button onclick="${this.dec}">-</button>
      <span>${this.count}</span>
      <button onclick="${this.inc}">+</button>
    `;
  }

  update() {
    render(this.shadowRoot, this.template());
  }
}

customElements.define('my-counter', MyCounter);

HTMLElement (w/ Lit-html)

Lit-Html - An efficient, expressive, extensible HTML templating library for JavaScript

Homepage: https://lit-html.polymer-project.org/

GitHub: https://github.com/polymer/lit-html

Try in WebComponents.dev
import { html, render } from 'lit-html';

class MyCounter extends HTMLElement {
  constructor() {
    super();
    this.count = 0;

    this.attachShadow({ mode: 'open' });
    this.update();
  }

  inc() {
    this.count++;
    this.update();
  }

  dec() {
    this.count--;
    this.update();
  }

  style() {
    return `
      * {
        font-size: 200%;
      }

      span {
        width: 4rem;
        display: inline-block;
        text-align: center;
      }

      button {
        width: 64px;
        height: 64px;
        border: none;
        border-radius: 10px;
        background-color: seagreen;
        color: white;
      }
    `;
  }

  template() {
    return html`
      <style>
        ${this.style()}
      </style>
      <button @click="${this.dec}">-</button>
      <span>${this.count}</span>
      <button @click="${this.inc}">+</button>
    `;
  }

  update() {
    render(this.template(), this.shadowRoot, { eventContext: this });
  }
}

customElements.define('my-counter', MyCounter);

HTMLElement (w/ uhtml)

micro html is a ~2.5K lighterhtml subset to build declarative and reactive UI via template literals tags.

Homepage: https://github.com/WebReflection/uhtml

GitHub: https://github.com/WebReflection/uhtml

Try in WebComponents.dev
import { html, render } from "uhtml";

class MyCounter extends HTMLElement {
  constructor() {
    super();
    this.count = 0;

    this.attachShadow({ mode: "open" });

    this.update();
  }

  inc = () => {
    this.count++;
    this.update();
  };

  dec = () => {
    this.count--;
    this.update();
  };

  template() {
    return html`
      <style>
        * {
          font-size: 200%;
        }

        span {
          width: 4rem;
          display: inline-block;
          text-align: center;
        }

        button {
          width: 64px;
          height: 64px;
          border: none;
          border-radius: 10px;
          background-color: seagreen;
          color: white;
        }
      </style>
      <button onclick="${this.dec}">-</button>
      <span>${this.count}</span>
      <button onclick="${this.inc}">+</button>
    `;
  }

  update() {
    render(this.shadowRoot, this.template());
  }
}

customElements.define("my-counter", MyCounter);

CanJS

Build CRUD apps in fewer lines of code.

Homepage: https://canjs.com/

GitHub: https://github.com/canjs/canjs

Try in WebComponents.dev
import { StacheElement } from "can";
// Extend Component to define a custom element
class MyCounter extends StacheElement {
  constructor() {
    super();
    this.viewRoot = this.attachShadow({ mode: "open" });
  }

  static view = `<style>
* {
  font-size: 200%;
}

span {
  width: 4rem;
  display: inline-block;
  text-align: center;
}

button {
  width: 64px;
  height: 64px;
  border: none;
  border-radius: 10px;
  background-color: seagreen;
  color: white;
}
</style>
<button on:click="this.decrement()">-</button>
<span>{{ this.count }}</span>
<button on:click="this.increment()">+</button>`;
  static props = {
    count: 0
  };
  increment() {
    this.count++;
  }
  decrement() {
    this.count--;
  }
}
customElements.define("my-counter", MyCounter);

HyperHTML Element

Framework agnostic, hyperHTML can be used to render any view, including Custom Elements and Web Components.

GitHub: https://github.com/WebReflection/hyperHTML-Element

Try in WebComponents.dev
import HyperHTMLElement from "hyperhtml-element";

class MyCounter extends HyperHTMLElement {
  constructor() {
    super();
    this.attachShadow({ mode: "open" });
  }

  created() {
    this.count = 0;
    this.render();
  }

  inc = () => {
    this.count++;
    this.render();
  };

  dec = () => {
    this.count--;
    this.render();
  };

  render() {
    return this.html`
    <style>
      * {
      font-size: 200%;
    }

    span {
      width: 4rem;
      display: inline-block;
      text-align: center;
    }

    button {
      width: 64px;
      height: 64px;
      border: none;
      border-radius: 10px;
      background-color: seagreen;
      color: white;
    }
    </style>
    <button onclick=${this.dec}>-</button>
    <span>${this.count}</span>
    <button onclick=${this.inc}>+</button>
    `;
  }
}

MyCounter.define("my-counter");

LitElement

Homepage: https://lit-element.polymer-project.org/

GitHub: https://github.com/polymer/lit-element

Try in WebComponents.dev
import { LitElement, html, css } from 'lit-element';

export class MyCounter extends LitElement {
  static properties = {
    count: { type: Number }
  };

  static styles = css`
    * {
      font-size: 200%;
    }

    span {
      width: 4rem;
      display: inline-block;
      text-align: center;
    }

    button {
      width: 64px;
      height: 64px;
      border: none;
      border-radius: 10px;
      background-color: seagreen;
      color: white;
    }
  `;

  constructor() {
    super();
    this.count = 0;
  }

  inc() {
    this.count++;
  }

  dec() {
    this.count--;
  }

  render() {
    return html`
      <button @click="${this.dec}">-</button>
      <span>${this.count}</span>
      <button @click="${this.inc}">+</button>
    `;
  }
}

customElements.define('my-counter', MyCounter);

Neow (Alpha)

Modern, fast, and light it's the front-end framework to fall inlove with. (ALPHA)

Homepage: https://neow.dev

GitHub: (alpha)

Try in WebComponents.dev
import { ComponentMixin } from "@neow/core";

class MyComponent extends ComponentMixin(HTMLElement) {
  static template = `
  <style>
    * {
      font-size: 200%;
    }

    span {
      width: 4rem;
      display: inline-block;
      text-align: center;
    }

    button {
      width: 64px;
      height: 64px;
      border: none;
      border-radius: 10px;
      background-color: seagreen;
      color: white;
    }
  </style>
  <button onclick="{{this.dec()}}">-</button>
  <span>{{this.counter}}</span>
  <button onclick="{{this.inc()}}">+</button>
`;

  counter = 0;

  inc() {
    this.counter++;
  }

  dec() {
    this.counter--;
  }
}

customElements.define("my-counter", MyComponent);

Omi w/Class

Front End Cross-Frameworks Framework - Merge Web Components, JSX, Virtual DOM, Functional style, observe or Proxy into one framework with tiny size and high performance. Write components once, using in everywhere, such as Omi, React, Preact, Vue or Angular.

Homepage: http://omijs.org

GitHub: https://github.com/Tencent/omi

Try in WebComponents.dev
import { define, WeElement, html } from "omi";

class MyCounter extends WeElement {
  static get propTypes() {
    return {
      count: Number
    };
  }

  static get defaultProps() {
    return { count: 0 };
  }

  install() {
    this.data = { count: this.props.count };
  }

  static get css() {
    return `
      * {
        font-size: 200%;
      }

      span {
        width: 4rem;
        display: inline-block;
        text-align: center;
      }

      button {
        width: 64px;
        height: 64px;
        border: none;
        border-radius: 10px;
        background-color: seagreen;
        color: white;
      }
    `;
  }

  inc = () => {
    this.data.count++;
    this.update();
  };

  dec = () => {
    this.data.count--;
    this.update();
  };

  render(props) {
    return html`
      <button onclick="${this.dec}">-</button>
      <span>${this.data.count}</span>
      <button onclick="${this.inc}">+</button>
    `;
  }
}

define("my-counter", MyCounter);

SkateJS (with Lit-html)

SkateJS - Effortless custom elements powered by modern view libraries.

Homepage: https://skatejs.netlify.com/

GitHub: https://github.com/skatejs/skatejs

Try in WebComponents.dev
import Element from "@skatejs/element";
import { render, html } from "lit-html";

class MyCounterElement extends Element {
  static get props() {
    return {
      count: Number
    };
  }

  inc = () => {
    this.count++;
  };

  dec = () => {
    this.count--;
  };

  render() {
    const style = `
    * {
      font-size: 200%;
    }

    span {
      width: 4rem;
      display: inline-block;
      text-align: center;
    }

    button {
      width: 64px;
      height: 64px;
      border: none;
      border-radius: 10px;
      background-color: seagreen;
      color: white;
    }`;

    return html`
      <style>
        ${style}
      </style>
      <button @click="${this.dec}">
        -
      </button>
      <span>${this.count}</span>
      <button @click="${this.inc}">
        +
      </button>
    `;
  }

  renderer() {
    return render(this.render(), this.renderRoot);
  }
}

customElements.define("my-counter", MyCounterElement);

SkateJS (with Preact)

SkateJS - Effortless custom elements powered by modern view libraries.

Homepage: https://skatejs.netlify.com/

GitHub: https://github.com/skatejs/skatejs

Try in WebComponents.dev
/** @jsx h **/
import Element, { h } from "@skatejs/element-preact";

class MyCounterElement extends Element {
  static get props() {
    return {
      count: Number
    };
  }

  inc = () => {
    this.count++;
  };

  dec = () => {
    this.count--;
  };

  render() {
    const style = `host * {
      font-size: 200%;
    }

    span {
      width: 4rem;
      display: inline-block;
      text-align: center;
    }

    button {
      width: 64px;
      height: 64px;
      border: none;
      border-radius: 10px;
      background-color: seagreen;
      color: white;
    }`;

    return (
      <host>
        <style>{style}</style>
        <button onclick={this.dec}>-</button>
        <span>{this.count}</span>
        <button onclick={this.inc}>+</button>
      </host>
    );
  }
}

customElements.define("my-counter", MyCounterElement);

SlimJS

Slim.js is a lightning fast library for development of native Web Components and Custom Elements based on modern standards. No black magic involved, no useless dependencies.

Homepage: https://slimjs.com

GitHub: https://github.com/slimjs/slim.js

Try in WebComponents.dev
import { Slim } from "slim-js/Slim.js";
import { tag, template, useShadow } from "slim-js/Decorators";

@tag("my-counter")
@useShadow(true)
@template(`
  <style>
    * {
      font-size: 200%;
    }

    span {
      width: 4rem;
      display: inline-block;
      text-align: center;
    }

    button {
      width: 64px;
      height: 64px;
      border: none;
      border-radius: 10px;
      background-color: seagreen;
      color: white;
    }
  </style >
  <button click="dec">-</button>
  <span>{{parseCount(count)}}</span>
  <button click="inc">+</button>
`)
class MyCounter extends Slim {
  constructor() {
    super();
    this.count = 0;
  }

  parseCount(num) {
    return String(num);
  }

  inc() {
    this.count++;
  }

  dec() {
    this.count--;
  }
}

Atomico

Atomico is a microlibrary (3.9kB) inspired by React Hooks, designed and optimized for the creation of small, powerful, declarative webcomponents and only using functions

Homepage: https://atomico.gitbook.io/doc/

GitHub: https://github.com/atomicojs/atomico

Try in WebComponents.dev
/** @jsx h */
import { h, customElement, useProp } from "atomico";

function MyCounter() {
  let [count, setCount] = useProp("count");

  const style = `
    * {
      font-size: 200%;
    }

    span {
      width: 4rem;
      display: inline-block;
      text-align: center;
    }

    button {
      width: 64px;
      height: 64px;
      border: none;
      border-radius: 10px;
      background-color: seagreen;
      color: white;
    }`;

  return (
    <host shadowDom>
      <style>{style}</style>
      <button onclick={() => setCount(count - 1)}>-</button>
      <span>{count}</span>
      <button onclick={() => setCount(count + 1)}>+</button>
    </host>
  );
}

MyCounter.props = {
  count: {
    type: Number,
    reflect: true,
    value: 0
  }
};

customElements.define("my-counter", customElement(MyCounter));

Haunted

React's Hooks API implemented for web components 👻

GitHub: https://github.com/matthewp/haunted

Try in WebComponents.dev
import { html } from "lit-html";
import { component, useState } from "haunted";

function Counter() {
  const [count, setCount] = useState(0);

  return html`
    <style>
      * {
        font-size: 200%;
      }

      span {
        width: 4rem;
        display: inline-block;
        text-align: center;
      }

      button {
        width: 64px;
        height: 64px;
        border: none;
        border-radius: 10px;
        background-color: seagreen;
        color: white;
      }
    </style>
    <button @click=${() => setCount(count - 1)}>
      -
    </button>
    <span>${count}</span>
    <button @click=${() => setCount(count + 1)}>
      +
    </button>
  `;
}

customElements.define("my-counter", component(Counter));

Heresy w/Hook

Don't simulate the DOM. Be the DOM. React-like Custom Elements via the V1 API built-in extends.

Homepage: https://medium.com/@WebReflection/any-holy-grail-for-web-components-c3d4973f3f3f

GitHub: https://github.com/WebReflection/heresy

Try in WebComponents.dev
import { define } from "heresy";

define("MyCounter", {
  style: MyCounter => `
    ${MyCounter} * {
      font-size: 200%;
    }
    ${MyCounter} span {
      width: 4rem;
      display: inline-block;
      text-align: center;
    }
    ${MyCounter} button {
      width: 64px;
      height: 64px;
      border: none;
      border-radius: 10px;
      background-color: seagreen;
      color: white;
    }
  `,
  render({ useState }) {
    const [count, update] = useState(0);
    this.html`
      <button onclick="${() => update(count - 1)}">-</button>
      <span>${count}</span>
      <button onclick="${() => update(count + 1)}">+</button>
    `;
  }
});

Heresy

Don't simulate the DOM. Be the DOM. React-like Custom Elements via the V1 API built-in extends.

Homepage: https://medium.com/@WebReflection/any-holy-grail-for-web-components-c3d4973f3f3f

GitHub: https://github.com/WebReflection/heresy

Try in WebComponents.dev
import { define } from "heresy";

define("MyCounter", {
  style: MyCounter => `
    ${MyCounter} * {
      font-size: 200%;
    }
    ${MyCounter} span {
      width: 4rem;
      display: inline-block;
      text-align: center;
    }
    ${MyCounter} button {
      width: 64px;
      height: 64px;
      border: none;
      border-radius: 10px;
      background-color: seagreen;
      color: white;
    }
  `,
  oninit() {
    this.count = 0;
  },
  onclick({ currentTarget }) {
    this[currentTarget.dataset.op]();
    this.render();
  },
  inc() {
    this.count++;
  },
  dec() {
    this.count--;
  },
  render() {
    this.html`
      <button data-op="dec" onclick="${this}">-</button>
      <span>${this.count}</span>
      <button data-op="inc" onclick="${this}">+</button>
    `;
  }
});

Hybrids

Hybrids is a UI library for creating web components with strong declarative and functional approach based on plain objects and pure functions.

Homepage: https://hybrids.js.org

GitHub: https://github.com/hybridsjs/hybrids

Try in WebComponents.dev
import { html, define } from "hybrids";

function inc(host) {
  host.count++;
}

function dec(host) {
  host.count--;
}

const MyCounter = {
  count: 0,
  render: ({ count }) => html`
    <style>
      * {
        font-size: 200%;
      }

      span {
        width: 4rem;
        display: inline-block;
        text-align: center;
      }

      button {
        width: 64px;
        height: 64px;
        border: none;
        border-radius: 10px;
        background-color: seagreen;
        color: white;
      }
    </style>
    <button onclick="${dec}">-</button>
    <span>${count}</span>
    <button onclick="${inc}">+</button>
  `
};

define("my-counter", MyCounter);

Litedom

A very small (3kb) view library that is packed: Web Components, Custom Element, Template Literals, Reactive, Data Binding, One Way Data Flow, Two-way data binding, Event Handling, Props, Lifecycle, State Management, Computed Properties, Directives. No dependencies, no virtual dom, no build tool. Wow! ...but it's a just a view library!

Homepage: https://litedom.js.org/

GitHub: https://github.com/mardix/litedom

Try in WebComponents.dev
import Litedom from "litedom";

Litedom({
  tagName: "my-counter",
  shadowDOM: true,
  template: `
  <button @click="dec">-</button>
  <span>{this.count}</span>
  <button @click="inc">+</button>
  <style>
    * {
      font-size: 200%;
    }

    span {
      width: 4rem;
      display: inline-block;
      text-align: center;
    }

    button {
      width: 64px;
      height: 64px;
      border: none;
      border-radius: 10px;
      background-color: seagreen;
      color: white;
    }
  </style>
  `,
  data: {
    count: 0
  },
  dec() {
    this.data.count--;
  },
  inc() {
    this.data.count++;
  }
});

Ottavino

Tiny, Fast and Declarative User Interface Development. Using native custom elements API (but not only). As simple as it gets.

GitHub: https://github.com/betterthancode/ottavino

Try in WebComponents.dev
import { component } from "ottavino";

component({
  tag: "my-counter",
  shadow: true,
  template: `
    <style>
      * {
        font-size: 200%;
      }

      span {
        width: 4rem;
        display: inline-block;
        text-align: center;
      }

      button {
        width: 64px;
        height: 64px;
        border: none;
        border-radius: 10px;
        background-color: seagreen;
        color: white;
      }
    </style>
    <button onclick="{{this.decrease()}}">-</button>
    <span>{{this.count}}</span>
    <button onclick="{{this.increase()}}" >+</button>
    `,
  properties: {
    count: 0
  },
  this: {
    increase: function() {
      this.count++;
    },
    decrease: function() {
      this.count--;
    }
  }
});

uce

µhtml based Custom Elements.

Homepage: https://github.com/WebReflection/uce

GitHub: https://github.com/WebReflection/uce

Try in WebComponents.dev
import { define } from "uce";

define("my-counter", {
  attachShadow: { mode: "open" },
  init() {
    this.count = 0;
    this.dec = () => {
      this.count--;
      this.render();
    };
    this.inc = () => {
      this.count++;
      this.render();
    };
    this.render();
  },
  render() {
    this.html`
      <style>
      * {
        font-size: 200%;
      }
      span {
        width: 4rem;
        display: inline-block;
        text-align: center;
      }
      button {
        width: 64px;
        height: 64px;
        border: none;
        border-radius: 10px;
        background-color: seagreen;
        color: white;
      }
      </style>
      <button onclick="${this.dec}">-</button>
      <span>${this.count}</span>
      <button onclick="${this.inc}">+</button>
    `;
  }
});

Lightning Web Components

⚡️ LWC - A Blazing Fast, Enterprise-Grade Web Components Foundation

Homepage: https://lwc.dev

GitHub: https://github.com/salesforce/lwc

Try in WebComponents.dev
import { LightningElement, api, buildCustomElementConstructor } from "lwc";

export default class MyCounter extends LightningElement {
  count = 0;

  inc() {
    this.count++;
  }

  dec() {
    this.count--;
  }
}

customElements.define("my-counter", buildCustomElementConstructor(MyCounter));

Solid Element

The Deceptively Simple User Interface Library

GitHub: https://github.com/ryansolid/solid

Try in WebComponents.dev
import { createSignal } from "solid-js";
import { customElement } from "solid-element";

const style = `div * {
          font-size: 200%;
        }

        span {
          width: 4rem;
          display: inline-block;
          text-align: center;
        }

        button {
          width: 4rem;
          height: 4rem;
          border: none;
          border-radius: 10px;
          background-color: seagreen;
          color: white;
        }`;

customElement("my-counter", () => {
  const [count, setCount] = createSignal(0);
  return (
    <div>
      <style>{style}</style>
      <button onClick={() => setCount(count() - 1)}>-</button>
      <span>{count}</span>
      <button onClick={() => setCount(count() + 1)}>+</button>
    </div>
  );
});

Stencil

Stencil is a toolchain for building reusable, scalable Design Systems. Generate small, blazing fast, and 100% standards based Web Components that run in every browser.

Homepage: https://stenciljs.com/

GitHub: https://github.com/ionic-team/stencil

Try in WebComponents.dev
/* @jsx h */
import { h, Component, State, Host } from "@stencil/core";

@Component({
  tag: "my-counter",
  styleUrl: "index.css",
  shadow: true
})
export class MyCounter {
  @State() count: number = 0;

  inc() {
    this.count++;
  }

  dec() {
    this.count--;
  }

  render() {
    return (
      <Host>
        <button onClick={this.dec.bind(this)}>-</button>
        <span>{this.count}</span>
        <button onClick={this.inc.bind(this)}>+</button>
      </Host>
    );
  }
}

Angular 9 Elements (wrapped with angular-elements)

One framework. Mobile & desktop.

Homepage: https://angular.io/

GitHub: https://github.com/angular/angular

Try in WebComponents.dev
import {
  Input,
  Component,
  ViewEncapsulation,
  ChangeDetectorRef
} from "@angular/core";

@Component({
  selector: "my-counter",
  template: `
    <button (click)="dec()">-</button>
    <span>{{count}}</span>
    <button (click)="inc()">+</button>
  `,
  styles: [
    `
      * {
        font-size: 200%;
      }

      span {
        width: 4rem;
        display: inline-block;
        text-align: center;
      }

      button {
        width: 64px;
        height: 64px;
        border: none;
        border-radius: 10px;
        background-color: seagreen;
        color: white;
      }
    `
  ],
  encapsulation: ViewEncapsulation.ShadowDom
})
export default class MyCounter {
  @Input() count: number = 0;

  constructor(private cd: ChangeDetectorRef) {}

  dec() {
    this.count--;
    this.cd.detectChanges();
  }

  inc() {
    this.count++;
    this.cd.detectChanges();
  }
}

Preact w/Class (wrapped with preact-custom-element)

Fast 3kB alternative to React with the same modern API.

Homepage: https://preactjs.com/

GitHub: https://github.com/preactjs/preact

Try in WebComponents.dev
import { createCustomElement } from "@wcd/preact-custom-element";
import { Component, html } from "htm/preact";
import "preact";

class MyCounter extends Component {
  state = {
    count: 0
  };

  inc = () => {
    this.setState(prev => ({ count: prev.count + 1 }));
  };

  dec = () => {
    this.setState(prev => ({ count: prev.count - 1 }));
  };

  render(props, state) {
    return html`
      <style>
        * {
          font-size: 200%;
        }

        span {
          width: 4rem;
          display: inline-block;
          text-align: center;
        }

        button {
          width: 64px;
          height: 64px;
          border: none;
          border-radius: 10px;
          background-color: seagreen;
          color: white;
        }
      </style>
      <button onClick=${this.dec}>
        -
      </button>
      <span>${state.count}</span>
      <button onClick=${this.inc}>
        +
      </button>
    `;
  }
}

customElements.define("my-counter", createCustomElement(MyCounter, ["count"]));

React w/Class (wrapped with react-to-webcomponent)

A JavaScript library for building user interfaces

Homepage: https://reactjs.org/

GitHub: https://github.com/facebook/react/

Try in WebComponents.dev
import React from "react";
import ReactDOM from "react-dom";
import reactToWebComponent from "react-to-webcomponent";

interface State {
  count: number;
}
interface Props {}

export default class MyCounter extends React.Component<Props, State> {
  constructor(props) {
    super(props);
    this.state = {
      count: 0
    };
  }

  render() {
    const styles = `.my-counter * {
      font-size: 200%;
    }

    .my-counter span {
      width: 4rem;
      display: inline-block;
      text-align: center;
    }

    .my-counter button {
      width: 64px;
      height: 64px;
      border: none;
      border-radius: 10px;
      background-color: seagreen;
      color: white;
    }`;

    return (
      <div className="my-counter">
        <style>{styles}</style>
        <button onClick={() => this.setState({ count: this.state.count - 1 })}>
          -
        </button>
        <span>{this.state.count}</span>
        <button onClick={() => this.setState({ count: this.state.count + 1 })}>
          +
        </button>
      </div>
    );
  }
}

customElements.define(
  "my-counter",
  reactToWebComponent(MyCounter, React, ReactDOM)
);

React w/Hook (wrapped with react-to-webcomponent)

A JavaScript library for building user interfaces

Homepage: https://reactjs.org/

GitHub: https://github.com/facebook/react/

Try in WebComponents.dev
import React, { useState } from "react";
import ReactDOM from "react-dom";
import reactToWebComponent from "react-to-webcomponent";

export default function MyCounter() {
  const [count, setCount] = useState(0);

  const styles = `
    .my-counter * {
      font-size: 200%;
    }

    .my-counter span {
      width: 4rem;
      display: inline-block;
      text-align: center;
    }

    .my-counter button {
      width: 64px;
      height: 64px;
      border: none;
      border-radius: 10px;
      background-color: seagreen;
      color: white;
    }`;

  return (
    <div className="my-counter">
      <style>{styles}</style>
      <button onClick={() => setCount(count - 1)}>-</button>
      <span>{count}</span>
      <button onClick={() => setCount(count + 1)}>+</button>
    </div>
  );
}

customElements.define(
  "my-counter",
  reactToWebComponent(MyCounter, React, ReactDOM)
);

Riot (wrapped with @riotjs/custom-elements)

Simple and elegant component-based UI library

Homepage: https://riot.js.org/

GitHub: https://github.com/riot/riot

Try in WebComponents.dev
<my-component>
	<style>
		* {
			font-size: 200%;
		}

		span {
			width: 4rem;
			display: inline-block;
			text-align: center;
		}

		button {
			width: 64px;
			height: 64px;
			border: none;
			border-radius: 10px;
			background-color: seagreen;
			color: white;
		}
	</style>

	<button onclick={dec}>
		-
	</button>
	<span>{state.count}</span>
	<button onclick={inc}>
		+
	</button>

	<script>
		export default {
      onBeforeMount(props, state) {
        this.state = {
					count: 0
        }
      },
      inc() {
        this.update({
          count: this.state.count+1
        })
      },
      dec() {
        this.update({
          count: this.state.count-1
        })
      },
    }
	</script>
</my-component>

Svelte (with {customElement: true})

Cybernetically enhanced web apps

Homepage: https://svelte.dev/

GitHub: https://github.com/sveltejs/svelte

Try in WebComponents.dev
<svelte:options tag="my-counter" />

<script>
	let count = 0;

	function inc() {
		count++;
	}

	function dec() {
		count--;
	}
</script>

<style>
	* {
		font-size: 200%;
	}

	span {
		width: 4rem;
		display: inline-block;
		text-align: center;
	}

	button {
		width: 64px;
		height: 64px;
		border: none;
		border-radius: 10px;
		background-color: seagreen;
		color: white;
	}
</style>

<button on:click={dec}>
	-
</button>
<span>{count}</span>
<button on:click={inc}>
	+
</button>

Vue.js (wrapped with vue-custom-element)

🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

Homepage: https://vuejs.org/

GitHub: https://github.com/vuejs/vue

Try in WebComponents.dev
<template>
	<div>
		<button @click="this.dec">
					-
		</button>
		<span>{{count}}</span>
		<button @click="this.inc">
					+
		</button>
	</div>
</template>

<script>
	export default {
		tag: 'my-counter',
		name: 'MyCounter',
		data() {
			return { count: 0 }
		},
		methods: {
			inc: function() {
				this.count++;
			},
			dec: function() {
				this.count--;
			}
		}
	};
</script>

<style scoped>
	span,
	button {
		font-size: 200%;
	}

	span {
		width: 4rem;
		display: inline-block;
		text-align: center;
	}

	button {
		width: 64px;
		height: 64px;
		border: none;
		border-radius: 10px;
		background-color: seagreen;
		color: white;
	}
</style>

Bundle Analysis

HTMLElement

No dependencies

Component size including library

Minified - Uncompressed 1,293
Minified + Gzip 558
Minified + Brotli 418

Composition

Open Bundle Visualizer

Compilation details

All Sources have been Transpiled by WebComponents.dev compiler with the following Babel settings:

[
  [
    Babel.availablePresets['env'],
    {
      targets: {
        browsers:
          'last 2 Chrome versions, last 2 Firefox versions, last 2 Safari versions, last 2 iOS versions, last 2 Android versions',
      },
      modules: 'false',
      bugfixes: true,
    },
  ],
  [Babel.availablePresets['react']],
],
plugins: [
  [Babel.availablePlugins['proposal-decorators'], { legacy: true }],
  [Babel.availablePlugins['proposal-class-properties'], { loose: true }],
]

Bundling details

Bundled with Rollup

import commonjs from '@rollup/plugin-commonjs';
import resolve from '@rollup/plugin-node-resolve';
import replace from '@rollup/plugin-replace';
import { terser } from 'rollup-plugin-terser';

...

    input: 'source.js',
    output: {
      dir: 'dist'),
      format: 'esm',
    },
    plugins: [
      replace({
        'process.env.NODE_ENV': JSON.stringify('production'),
      }),
      withdeps ? resolve() : undefined,
      commonjs( /* { namedExports: for React } */ ),
      terser({ output: { comments: false } }),

...

HTMLElement (w/ Lighterhtml)

Dependencies

lighterhtml : ^3.0.0

Component size including library

Minified - Uncompressed 14,479
Minified + Gzip 6,350
Minified + Brotli 5,755

Composition

Open Bundle Visualizer

Compilation details

All Sources have been Transpiled by WebComponents.dev compiler with the following Babel settings:

[
  [
    Babel.availablePresets['env'],
    {
      targets: {
        browsers:
          'last 2 Chrome versions, last 2 Firefox versions, last 2 Safari versions, last 2 iOS versions, last 2 Android versions',
      },
      modules: 'false',
      bugfixes: true,
    },
  ],
  [Babel.availablePresets['react']],
],
plugins: [
  [Babel.availablePlugins['proposal-decorators'], { legacy: true }],
  [Babel.availablePlugins['proposal-class-properties'], { loose: true }],
]

Bundling details

Bundled with Rollup

import commonjs from '@rollup/plugin-commonjs';
import resolve from '@rollup/plugin-node-resolve';
import replace from '@rollup/plugin-replace';
import { terser } from 'rollup-plugin-terser';

...

    input: 'source.js',
    output: {
      dir: 'dist'),
      format: 'esm',
    },
    plugins: [
      replace({
        'process.env.NODE_ENV': JSON.stringify('production'),
      }),
      withdeps ? resolve() : undefined,
      commonjs( /* { namedExports: for React } */ ),
      terser({ output: { comments: false } }),

...

HTMLElement (w/ Lit-html)

Dependencies

lit-html : ^1.2.1

Component size including library

Minified - Uncompressed 9,958
Minified + Gzip 3,544
Minified + Brotli 3,158

Composition

Open Bundle Visualizer

Compilation details

All Sources have been Transpiled by WebComponents.dev compiler with the following Babel settings:

[
  [
    Babel.availablePresets['env'],
    {
      targets: {
        browsers:
          'last 2 Chrome versions, last 2 Firefox versions, last 2 Safari versions, last 2 iOS versions, last 2 Android versions',
      },
      modules: 'false',
      bugfixes: true,
    },
  ],
  [Babel.availablePresets['react']],
],
plugins: [
  [Babel.availablePlugins['proposal-decorators'], { legacy: true }],
  [Babel.availablePlugins['proposal-class-properties'], { loose: true }],
]

Bundling details

Bundled with Rollup

import commonjs from '@rollup/plugin-commonjs';
import resolve from '@rollup/plugin-node-resolve';
import replace from '@rollup/plugin-replace';
import { terser } from 'rollup-plugin-terser';

...

    input: 'source.js',
    output: {
      dir: 'dist'),
      format: 'esm',
    },
    plugins: [
      replace({
        'process.env.NODE_ENV': JSON.stringify('production'),
      }),
      withdeps ? resolve() : undefined,
      commonjs( /* { namedExports: for React } */ ),
      terser({ output: { comments: false } }),

...

HTMLElement (w/ uhtml)

Dependencies

uhtml : ^1.8.1

Component size including library

Minified - Uncompressed 7,135
Minified + Gzip 3,236
Minified + Brotli 2,910

Composition

Open Bundle Visualizer

Compilation details

All Sources have been Transpiled by WebComponents.dev compiler with the following Babel settings:

[
  [
    Babel.availablePresets['env'],
    {
      targets: {
        browsers:
          'last 2 Chrome versions, last 2 Firefox versions, last 2 Safari versions, last 2 iOS versions, last 2 Android versions',
      },
      modules: 'false',
      bugfixes: true,
    },
  ],
  [Babel.availablePresets['react']],
],
plugins: [
  [Babel.availablePlugins['proposal-decorators'], { legacy: true }],
  [Babel.availablePlugins['proposal-class-properties'], { loose: true }],
]

Bundling details

Bundled with Rollup

import commonjs from '@rollup/plugin-commonjs';
import resolve from '@rollup/plugin-node-resolve';
import replace from '@rollup/plugin-replace';
import { terser } from 'rollup-plugin-terser';

...

    input: 'source.js',
    output: {
      dir: 'dist'),
      format: 'esm',
    },
    plugins: [
      replace({
        'process.env.NODE_ENV': JSON.stringify('production'),
      }),
      withdeps ? resolve() : undefined,
      commonjs( /* { namedExports: for React } */ ),
      terser({ output: { comments: false } }),

...

CanJS

Dependencies

can : ^6.4.0

Component size including library

Minified - Uncompressed 230,475
Minified + Gzip 63,084
Minified + Brotli 54,576

Composition

Open Bundle Visualizer

Compilation details

All Sources have been Transpiled by WebComponents.dev compiler with the following Babel settings:

[
  [
    Babel.availablePresets['env'],
    {
      targets: {
        browsers:
          'last 2 Chrome versions, last 2 Firefox versions, last 2 Safari versions, last 2 iOS versions, last 2 Android versions',
      },
      modules: 'false',
      bugfixes: true,
    },
  ],
  [Babel.availablePresets['react']],
],
plugins: [
  [Babel.availablePlugins['proposal-decorators'], { legacy: true }],
  [Babel.availablePlugins['proposal-class-properties'], { loose: true }],
]

Bundling details

Bundled with Rollup

import commonjs from '@rollup/plugin-commonjs';
import resolve from '@rollup/plugin-node-resolve';
import replace from '@rollup/plugin-replace';
import { terser } from 'rollup-plugin-terser';

...

    input: 'source.js',
    output: {
      dir: 'dist'),
      format: 'esm',
    },
    plugins: [
      replace({
        'process.env.NODE_ENV': JSON.stringify('production'),
      }),
      withdeps ? resolve() : undefined,
      commonjs( /* { namedExports: for React } */ ),
      terser({ output: { comments: false } }),

...

HyperHTML Element

Dependencies

hyperhtml-element : ^3.12.3

Component size including library

Minified - Uncompressed 22,607
Minified + Gzip 9,051
Minified + Brotli 8,282

Composition

Open Bundle Visualizer

Compilation details

All Sources have been Transpiled by WebComponents.dev compiler with the following Babel settings:

[
  [
    Babel.availablePresets['env'],
    {
      targets: {
        browsers:
          'last 2 Chrome versions, last 2 Firefox versions, last 2 Safari versions, last 2 iOS versions, last 2 Android versions',
      },
      modules: 'false',
      bugfixes: true,
    },
  ],
  [Babel.availablePresets['react']],
],
plugins: [
  [Babel.availablePlugins['proposal-decorators'], { legacy: true }],
  [Babel.availablePlugins['proposal-class-properties'], { loose: true }],
]

Bundling details

Bundled with Rollup

import commonjs from '@rollup/plugin-commonjs';
import resolve from '@rollup/plugin-node-resolve';
import replace from '@rollup/plugin-replace';
import { terser } from 'rollup-plugin-terser';

...

    input: 'source.js',
    output: {
      dir: 'dist'),
      format: 'esm',
    },
    plugins: [
      replace({
        'process.env.NODE_ENV': JSON.stringify('production'),
      }),
      withdeps ? resolve() : undefined,
      commonjs( /* { namedExports: for React } */ ),
      terser({ output: { comments: false } }),

...

LitElement

Dependencies

lit-element : ^2.3.1

Component size including library

Minified - Uncompressed 20,416
Minified + Gzip 6,706
Minified + Brotli 6,024

Composition

Open Bundle Visualizer

Compilation details

All Sources have been Transpiled by WebComponents.dev compiler with the following Babel settings:

[
  [
    Babel.availablePresets['env'],
    {
      targets: {
        browsers:
          'last 2 Chrome versions, last 2 Firefox versions, last 2 Safari versions, last 2 iOS versions, last 2 Android versions',
      },
      modules: 'false',
      bugfixes: true,
    },
  ],
  [Babel.availablePresets['react']],
],
plugins: [
  [Babel.availablePlugins['proposal-decorators'], { legacy: true }],
  [Babel.availablePlugins['proposal-class-properties'], { loose: true }],
]

Bundling details

Bundled with Rollup

import commonjs from '@rollup/plugin-commonjs';
import resolve from '@rollup/plugin-node-resolve';
import replace from '@rollup/plugin-replace';
import { terser } from 'rollup-plugin-terser';

...

    input: 'source.js',
    output: {
      dir: 'dist'),
      format: 'esm',
    },
    plugins: [
      replace({
        'process.env.NODE_ENV': JSON.stringify('production'),
      }),
      withdeps ? resolve() : undefined,
      commonjs( /* { namedExports: for React } */ ),
      terser({ output: { comments: false } }),

...

Neow (Alpha)

Dependencies

@neow/core : 0.0.6

Component size including library

Minified - Uncompressed 4,435
Minified + Gzip 1,879
Minified + Brotli 1,617

Composition

Open Bundle Visualizer

Compilation details

All Sources have been Transpiled by WebComponents.dev compiler with the following Babel settings:

[
  [
    Babel.availablePresets['env'],
    {
      targets: {
        browsers:
          'last 2 Chrome versions, last 2 Firefox versions, last 2 Safari versions, last 2 iOS versions, last 2 Android versions',
      },
      modules: 'false',
      bugfixes: true,
    },
  ],
  [Babel.availablePresets['react']],
],
plugins: [
  [Babel.availablePlugins['proposal-decorators'], { legacy: true }],
  [Babel.availablePlugins['proposal-class-properties'], { loose: true }],
]

Bundling details

Bundled with Rollup

import commonjs from '@rollup/plugin-commonjs';
import resolve from '@rollup/plugin-node-resolve';
import replace from '@rollup/plugin-replace';
import { terser } from 'rollup-plugin-terser';

...

    input: 'source.js',
    output: {
      dir: 'dist'),
      format: 'esm',
    },
    plugins: [
      replace({
        'process.env.NODE_ENV': JSON.stringify('production'),
      }),
      withdeps ? resolve() : undefined,
      commonjs( /* { namedExports: for React } */ ),
      terser({ output: { comments: false } }),

...

Omi w/Class

Dependencies

omi : ^6.17.0

Component size including library

Minified - Uncompressed 24,627
Minified + Gzip 8,404
Minified + Brotli 7,576

Composition

Open Bundle Visualizer

Compilation details

All Sources have been Transpiled by WebComponents.dev compiler with the following Babel settings:

[
  [
    Babel.availablePresets['env'],
    {
      targets: {
        browsers:
          'last 2 Chrome versions, last 2 Firefox versions, last 2 Safari versions, last 2 iOS versions, last 2 Android versions',
      },
      modules: 'false',
      bugfixes: true,
    },
  ],
  [Babel.availablePresets['react']],
],
plugins: [
  [Babel.availablePlugins['proposal-decorators'], { legacy: true }],
  [Babel.availablePlugins['proposal-class-properties'], { loose: true }],
]

Bundling details

Bundled with Rollup

import commonjs from '@rollup/plugin-commonjs';
import resolve from '@rollup/plugin-node-resolve';
import replace from '@rollup/plugin-replace';
import { terser } from 'rollup-plugin-terser';

...

    input: 'source.js',
    output: {
      dir: 'dist'),
      format: 'esm',
    },
    plugins: [
      replace({
        'process.env.NODE_ENV': JSON.stringify('production'),
      }),
      withdeps ? resolve() : undefined,
      commonjs( /* { namedExports: for React } */ ),
      terser({ output: { comments: false } }),

...

SkateJS (with Lit-html)

Dependencies

@skatejs/element : 0.0.1
lit-html : ^1.2.1

Component size including library

Minified - Uncompressed 15,424
Minified + Gzip 5,168
Minified + Brotli 4,628

Composition

Open Bundle Visualizer

Compilation details

All Sources have been Transpiled by WebComponents.dev compiler with the following Babel settings:

[
  [
    Babel.availablePresets['env'],
    {
      targets: {
        browsers:
          'last 2 Chrome versions, last 2 Firefox versions, last 2 Safari versions, last 2 iOS versions, last 2 Android versions',
      },
      modules: 'false',
      bugfixes: true,
    },
  ],
  [Babel.availablePresets['react']],
],
plugins: [
  [Babel.availablePlugins['proposal-decorators'], { legacy: true }],
  [Babel.availablePlugins['proposal-class-properties'], { loose: true }],
]

Bundling details

Bundled with Rollup

import commonjs from '@rollup/plugin-commonjs';
import resolve from '@rollup/plugin-node-resolve';
import replace from '@rollup/plugin-replace';
import { terser } from 'rollup-plugin-terser';

...

    input: 'source.js',
    output: {
      dir: 'dist'),
      format: 'esm',
    },
    plugins: [
      replace({
        'process.env.NODE_ENV': JSON.stringify('production'),
      }),
      withdeps ? resolve() : undefined,
      commonjs( /* { namedExports: for React } */ ),
      terser({ output: { comments: false } }),

...

SkateJS (with Preact)

Dependencies

@skatejs/element-preact : 0.0.1

Component size including library

Minified - Uncompressed 17,079
Minified + Gzip 6,022
Minified + Brotli 5,466

Composition

Open Bundle Visualizer

Compilation details

All Sources have been Transpiled by WebComponents.dev compiler with the following Babel settings:

[
  [
    Babel.availablePresets['env'],
    {
      targets: {
        browsers:
          'last 2 Chrome versions, last 2 Firefox versions, last 2 Safari versions, last 2 iOS versions, last 2 Android versions',
      },
      modules: 'false',
      bugfixes: true,
    },
  ],
  [Babel.availablePresets['react']],
],
plugins: [
  [Babel.availablePlugins['proposal-decorators'], { legacy: true }],
  [Babel.availablePlugins['proposal-class-properties'], { loose: true }],
]

Bundling details

Bundled with Rollup

import commonjs from '@rollup/plugin-commonjs';
import resolve from '@rollup/plugin-node-resolve';
import replace from '@rollup/plugin-replace';
import { terser } from 'rollup-plugin-terser';

...

    input: 'source.js',
    output: {
      dir: 'dist'),
      format: 'esm',
    },
    plugins: [
      replace({
        'process.env.NODE_ENV': JSON.stringify('production'),
      }),
      withdeps ? resolve() : undefined,
      commonjs( /* { namedExports: for React } */ ),
      terser({ output: { comments: false } }),

...

SlimJS

Dependencies

slim-js : ^4.0.7

Component size including library

Minified - Uncompressed 9,467
Minified + Gzip 3,661
Minified + Brotli 3,220

Composition

Open Bundle Visualizer

Compilation details

All Sources have been Transpiled by WebComponents.dev compiler with the following Babel settings:

[
  [
    Babel.availablePresets['env'],
    {
      targets: {
        browsers:
          'last 2 Chrome versions, last 2 Firefox versions, last 2 Safari versions, last 2 iOS versions, last 2 Android versions',
      },
      modules: 'false',
      bugfixes: true,
    },
  ],
  [Babel.availablePresets['react']],
],
plugins: [
  [Babel.availablePlugins['proposal-decorators'], { legacy: true }],
  [Babel.availablePlugins['proposal-class-properties'], { loose: true }],
]

Bundling details

Bundled with Rollup

import commonjs from '@rollup/plugin-commonjs';
import resolve from '@rollup/plugin-node-resolve';
import replace from '@rollup/plugin-replace';
import { terser } from 'rollup-plugin-terser';

...

    input: 'source.js',
    output: {
      dir: 'dist'),
      format: 'esm',
    },
    plugins: [
      replace({
        'process.env.NODE_ENV': JSON.stringify('production'),
      }),
      withdeps ? resolve() : undefined,
      commonjs( /* { namedExports: for React } */ ),
      terser({ output: { comments: false } }),

...

Atomico

Dependencies

atomico : ^0.21.1

Component size including library

Minified - Uncompressed 8,032
Minified + Gzip 3,690
Minified + Brotli 3,285

Composition

Open Bundle Visualizer

Compilation details

All Sources have been Transpiled by WebComponents.dev compiler with the following Babel settings:

[
  [
    Babel.availablePresets['env'],
    {
      targets: {
        browsers:
          'last 2 Chrome versions, last 2 Firefox versions, last 2 Safari versions, last 2 iOS versions, last 2 Android versions',
      },
      modules: 'false',
      bugfixes: true,
    },
  ],
  [Babel.availablePresets['react']],
],
plugins: [
  [Babel.availablePlugins['proposal-decorators'], { legacy: true }],
  [Babel.availablePlugins['proposal-class-properties'], { loose: true }],
]

Bundling details

Bundled with Rollup

import commonjs from '@rollup/plugin-commonjs';
import resolve from '@rollup/plugin-node-resolve';
import replace from '@rollup/plugin-replace';
import { terser } from 'rollup-plugin-terser';

...

    input: 'source.js',
    output: {
      dir: 'dist'),
      format: 'esm',
    },
    plugins: [
      replace({
        'process.env.NODE_ENV': JSON.stringify('production'),
      }),
      withdeps ? resolve() : undefined,
      commonjs( /* { namedExports: for React } */ ),
      terser({ output: { comments: false } }),

...

Haunted

Dependencies

haunted : ^4.7.0

Component size including library

Minified - Uncompressed 15,331
Minified + Gzip 5,283
Minified + Brotli 4,722

Composition

Open Bundle Visualizer

Compilation details

All Sources have been Transpiled by WebComponents.dev compiler with the following Babel settings:

[
  [
    Babel.availablePresets['env'],
    {
      targets: {
        browsers:
          'last 2 Chrome versions, last 2 Firefox versions, last 2 Safari versions, last 2 iOS versions, last 2 Android versions',
      },
      modules: 'false',
      bugfixes: true,
    },
  ],
  [Babel.availablePresets['react']],
],
plugins: [
  [Babel.availablePlugins['proposal-decorators'], { legacy: true }],
  [Babel.availablePlugins['proposal-class-properties'], { loose: true }],
]

Bundling details

Bundled with Rollup

import commonjs from '@rollup/plugin-commonjs';
import resolve from '@rollup/plugin-node-resolve';
import replace from '@rollup/plugin-replace';
import { terser } from 'rollup-plugin-terser';

...

    input: 'source.js',
    output: {
      dir: 'dist'),
      format: 'esm',
    },
    plugins: [
      replace({
        'process.env.NODE_ENV': JSON.stringify('production'),
      }),
      withdeps ? resolve() : undefined,
      commonjs( /* { namedExports: for React } */ ),
      terser({ output: { comments: false } }),

...

Heresy w/Hook

Dependencies

heresy : ^0.27.0

Component size including library

Minified - Uncompressed 23,772
Minified + Gzip 9,881
Minified + Brotli 9,002

Composition

Open Bundle Visualizer

Compilation details

All Sources have been Transpiled by WebComponents.dev compiler with the following Babel settings:

[
  [
    Babel.availablePresets['env'],
    {
      targets: {
        browsers:
          'last 2 Chrome versions, last 2 Firefox versions, last 2 Safari versions, last 2 iOS versions, last 2 Android versions',
      },
      modules: 'false',
      bugfixes: true,
    },
  ],
  [Babel.availablePresets['react']],
],
plugins: [
  [Babel.availablePlugins['proposal-decorators'], { legacy: true }],
  [Babel.availablePlugins['proposal-class-properties'], { loose: true }],
]

Bundling details

Bundled with Rollup

import commonjs from '@rollup/plugin-commonjs';
import resolve from '@rollup/plugin-node-resolve';
import replace from '@rollup/plugin-replace';
import { terser } from 'rollup-plugin-terser';

...

    input: 'source.js',
    output: {
      dir: 'dist'),
      format: 'esm',
    },
    plugins: [
      replace({
        'process.env.NODE_ENV': JSON.stringify('production'),
      }),
      withdeps ? resolve() : undefined,
      commonjs( /* { namedExports: for React } */ ),
      terser({ output: { comments: false } }),

...

Heresy

Dependencies

heresy : ^0.27.0

Component size including library

Minified - Uncompressed 23,895
Minified + Gzip 9,918
Minified + Brotli 9,011

Composition

Open Bundle Visualizer

Compilation details

All Sources have been Transpiled by WebComponents.dev compiler with the following Babel settings:

[
  [
    Babel.availablePresets['env'],
    {
      targets: {
        browsers:
          'last 2 Chrome versions, last 2 Firefox versions, last 2 Safari versions, last 2 iOS versions, last 2 Android versions',
      },
      modules: 'false',
      bugfixes: true,
    },
  ],
  [Babel.availablePresets['react']],
],
plugins: [
  [Babel.availablePlugins['proposal-decorators'], { legacy: true }],
  [Babel.availablePlugins['proposal-class-properties'], { loose: true }],
]

Bundling details

Bundled with Rollup

import commonjs from '@rollup/plugin-commonjs';
import resolve from '@rollup/plugin-node-resolve';
import replace from '@rollup/plugin-replace';
import { terser } from 'rollup-plugin-terser';

...

    input: 'source.js',
    output: {
      dir: 'dist'),
      format: 'esm',
    },
    plugins: [
      replace({
        'process.env.NODE_ENV': JSON.stringify('production'),
      }),
      withdeps ? resolve() : undefined,
      commonjs( /* { namedExports: for React } */ ),
      terser({ output: { comments: false } }),

...

Hybrids

Dependencies

hybrids : ^4.1.7

Component size including library

Minified - Uncompressed 19,704
Minified + Gzip 6,527
Minified + Brotli 5,892

Composition

Open Bundle Visualizer

Compilation details

All Sources have been Transpiled by WebComponents.dev compiler with the following Babel settings:

[
  [
    Babel.availablePresets['env'],
    {
      targets: {
        browsers:
          'last 2 Chrome versions, last 2 Firefox versions, last 2 Safari versions, last 2 iOS versions, last 2 Android versions',
      },
      modules: 'false',
      bugfixes: true,
    },
  ],
  [Babel.availablePresets['react']],
],
plugins: [
  [Babel.availablePlugins['proposal-decorators'], { legacy: true }],
  [Babel.availablePlugins['proposal-class-properties'], { loose: true }],
]

Bundling details

Bundled with Rollup

import commonjs from '@rollup/plugin-commonjs';
import resolve from '@rollup/plugin-node-resolve';
import replace from '@rollup/plugin-replace';
import { terser } from 'rollup-plugin-terser';

...

    input: 'source.js',
    output: {
      dir: 'dist'),
      format: 'esm',
    },
    plugins: [
      replace({
        'process.env.NODE_ENV': JSON.stringify('production'),
      }),
      withdeps ? resolve() : undefined,
      commonjs( /* { namedExports: for React } */ ),
      terser({ output: { comments: false } }),

...

Litedom

Dependencies

litedom : ^0.12.1

Component size including library

Minified - Uncompressed 9,411
Minified + Gzip 3,847
Minified + Brotli 3,422

Composition

Open Bundle Visualizer

Compilation details

All Sources have been Transpiled by WebComponents.dev compiler with the following Babel settings:

[
  [
    Babel.availablePresets['env'],
    {
      targets: {
        browsers:
          'last 2 Chrome versions, last 2 Firefox versions, last 2 Safari versions, last 2 iOS versions, last 2 Android versions',
      },
      modules: 'false',
      bugfixes: true,
    },
  ],
  [Babel.availablePresets['react']],
],
plugins: [
  [Babel.availablePlugins['proposal-decorators'], { legacy: true }],
  [Babel.availablePlugins['proposal-class-properties'], { loose: true }],
]

Bundling details

Bundled with Rollup

import commonjs from '@rollup/plugin-commonjs';
import resolve from '@rollup/plugin-node-resolve';
import replace from '@rollup/plugin-replace';
import { terser } from 'rollup-plugin-terser';

...

    input: 'source.js',
    output: {
      dir: 'dist'),
      format: 'esm',
    },
    plugins: [
      replace({
        'process.env.NODE_ENV': JSON.stringify('production'),
      }),
      withdeps ? resolve() : undefined,
      commonjs( /* { namedExports: for React } */ ),
      terser({ output: { comments: false } }),

...

Ottavino

Dependencies

ottavino : ^0.2.4

Component size including library

Minified - Uncompressed 4,446
Minified + Gzip 1,985
Minified + Brotli 1,717

Composition

Open Bundle Visualizer

Compilation details

All Sources have been Transpiled by WebComponents.dev compiler with the following Babel settings:

[
  [
    Babel.availablePresets['env'],
    {
      targets: {
        browsers:
          'last 2 Chrome versions, last 2 Firefox versions, last 2 Safari versions, last 2 iOS versions, last 2 Android versions',
      },
      modules: 'false',
      bugfixes: true,
    },
  ],
  [Babel.availablePresets['react']],
],
plugins: [
  [Babel.availablePlugins['proposal-decorators'], { legacy: true }],
  [Babel.availablePlugins['proposal-class-properties'], { loose: true }],
]

Bundling details

Bundled with Rollup

import commonjs from '@rollup/plugin-commonjs';
import resolve from '@rollup/plugin-node-resolve';
import replace from '@rollup/plugin-replace';
import { terser } from 'rollup-plugin-terser';

...

    input: 'source.js',
    output: {
      dir: 'dist'),
      format: 'esm',
    },
    plugins: [
      replace({
        'process.env.NODE_ENV': JSON.stringify('production'),
      }),
      withdeps ? resolve() : undefined,
      commonjs( /* { namedExports: for React } */ ),
      terser({ output: { comments: false } }),

...

uce

Dependencies

uce :

Component size including library

Minified - Uncompressed 8,156
Minified + Gzip 3,657
Minified + Brotli 3,293

Composition

Open Bundle Visualizer

Compilation details

All Sources have been Transpiled by WebComponents.dev compiler with the following Babel settings:

[
  [
    Babel.availablePresets['env'],
    {
      targets: {
        browsers:
          'last 2 Chrome versions, last 2 Firefox versions, last 2 Safari versions, last 2 iOS versions, last 2 Android versions',
      },
      modules: 'false',
      bugfixes: true,
    },
  ],
  [Babel.availablePresets['react']],
],
plugins: [
  [Babel.availablePlugins['proposal-decorators'], { legacy: true }],
  [Babel.availablePlugins['proposal-class-properties'], { loose: true }],
]

Bundling details

Bundled with Rollup

import commonjs from '@rollup/plugin-commonjs';
import resolve from '@rollup/plugin-node-resolve';
import replace from '@rollup/plugin-replace';
import { terser } from 'rollup-plugin-terser';

...

    input: 'source.js',
    output: {
      dir: 'dist'),
      format: 'esm',
    },
    plugins: [
      replace({
        'process.env.NODE_ENV': JSON.stringify('production'),
      }),
      withdeps ? resolve() : undefined,
      commonjs( /* { namedExports: for React } */ ),
      terser({ output: { comments: false } }),

...

Lightning Web Components

Dependencies

lwc : ^1.3.2

Component size including library

Minified - Uncompressed 36,565
Minified + Gzip 12,644
Minified + Brotli 11,335

Composition

Open Bundle Visualizer

Compilation details

All Sources have been Transpiled by WebComponents.dev compiler with the following LWC settings:

const outputConfig: {
  format: 'esm',
  sourcemap: true,
};

Bundling details

Bundled with Rollup

import commonjs from '@rollup/plugin-commonjs';
import resolve from '@rollup/plugin-node-resolve';
import replace from '@rollup/plugin-replace';
import { terser } from 'rollup-plugin-terser';

...

    input: 'source.js',
    output: {
      dir: 'dist'),
      format: 'esm',
    },
    plugins: [
      replace({
        'process.env.NODE_ENV': JSON.stringify('production'),
      }),
      withdeps ? resolve() : undefined,
      commonjs( /* { namedExports: for React } */ ),
      terser({ output: { comments: false } }),

...

Solid Element

Dependencies

solid-js : ^0.17.2
solid-element : ^0.17.2

Component size including library

Minified - Uncompressed 12,494
Minified + Gzip 4,798
Minified + Brotli 4,268

Composition

Open Bundle Visualizer

Compilation details

All Sources have been Transpiled by WebComponents.dev compiler with the solid preset:

{
  "presets": [
    "solid"
  ]
}

Bundling details

Bundled with Rollup

import commonjs from '@rollup/plugin-commonjs';
import resolve from '@rollup/plugin-node-resolve';
import replace from '@rollup/plugin-replace';
import { terser } from 'rollup-plugin-terser';

...

    input: 'source.js',
    output: {
      dir: 'dist'),
      format: 'esm',
    },
    plugins: [
      replace({
        'process.env.NODE_ENV': JSON.stringify('production'),
      }),
      withdeps ? resolve() : undefined,
      commonjs( /* { namedExports: for React } */ ),
      terser({ output: { comments: false } }),

...

Stencil

Dependencies

@stencil/core : ^1.12.1

Component size including library

Minified - Uncompressed 6,786
Minified + Gzip 3,558
Minified + Brotli 3,169

Composition

Open Bundle Visualizer

Compilation details

All Sources have been compiled with Stencil CLI and settings:

// tsconfig.js
...
"compilerOptions": {
  "allowSyntheticDefaultImports": true,
  "allowUnreachableCode": false,
  "declaration": false,
  "experimentalDecorators": true,
  "lib": ["DOM", "ES2018"],
  "moduleResolution": "node",
  "module": "esnext",
  "target": "es2017",
  "noUnusedLocals": true,
  "noUnusedParameters": true,
  "jsx": "react",
  "jsxFactory": "h",
  "types": []
}
...

//stencil.config.js
import { Config } from '@stencil/core';
export const config: Config = {
  srcDir: 'stencil/src',
  namespace: 'my-counter',
  hashFileNames: false,
  plugins: [],
  outputTargets: [
    {
      type: 'www',
      serviceWorker: null, // disable service workers
    },
  ],
  hydratedFlag: null,
  extras: {
    cssVarsShim: false,
    dynamicImportShim: false,
    safari10: false,
    scriptDataOpts: false,
    shadowDomShim: false,
  },
};

Bundling details

Bundled by Stencil CLI

Angular 9 Elements

Dependencies

@angular/core : ^9.0.7
@angular/cli : ^9.1.0
@angular/common : ^9.0.7
@angular/compiler : ^9.0.7
@angular/compiler-cli : ^9.1.0
@angular-devkit/build-angular : ^0.901.0
document-register-element : ^1.14.3
@angular/elements : ^9.0.7
@angular/platform-browser-dynamic : ^9.0.7
@angular/platform-browser : ^9.0.7

Component size including library

Minified - Uncompressed 225,870
Minified + Gzip 69,366
Minified + Brotli 59,540

Composition

Open Bundle Visualizer

Compilation details

Compile with Angular compiler and the following angular.json:

{
  "$schema": "./node_modules/@angular/cli/lib/config/schema.json",
  "version": 1,
  "newProjectRoot": "download",
  "projects": {
    "elements": {
      "root": "download/007-angular-9-elements",
      "sourceRoot": "download/007-angular-9-elements/src",
      "projectType": "application",
      "prefix": "app",
      "schematics": {},
      "architect": {
        "build": {
          "builder": "@angular-devkit/build-angular:browser",
          "options": {
            "aot": true,
            "outputPath": "download/007-angular-9-elements/dist",
            "index": "download/007-angular-9-elements/src/index.html",
            "main": "download/007-angular-9-elements/src/index.ts",
            "tsConfig": "angular.tsconfig.json",
            "polyfills": "polyfills.ts",
            "assets": [],
            "styles": [],
            "scripts": [
              {
                "input": "node_modules/document-register-element/build/document-register-element.js"
              }
            ]
          },
          "configurations": {
            "production": {
              "optimization": true,
              "outputHashing": "none",
              "sourceMap": true,
              "extractCss": true,
              "namedChunks": false,
              "aot": true,
              "extractLicenses": false,
              "vendorChunk": true,
              "buildOptimizer": true
            }
          }
        }
      }
    }
  },
  "defaultProject": "elements",
  "schematics": {
    "@schematics/angular:component": {
      "prefix": "app",
      "style": "css"
    },
    "@schematics/angular:directive": {
      "prefix": "app"
    }
  }
}

And following tsconfig.json:


{
  "compileOnSave": false,
  "compilerOptions": {
    "module": "esnext",
    "outDir": "./download/007-angular-9-elements/dist/out-tsc",
    "baseUrl": "download/007-angular-9-elements/src",
    "sourceMap": true,
    "declaration": false,
    "moduleResolution": "node",
    "emitDecoratorMetadata": true,
    "experimentalDecorators": true,
    "target": "es2015",
    "typeRoots": ["node_modules/@types"],
    "lib": ["es2016", "dom"],
    "paths": {
      "EnumUtils": ["dist/enum-utils"],
      "EnumUtils/*": ["dist/enum-utils/*"]
    }
  },
  "files": ["download/007-angular-9-elements/src/index.ts", "polyfills.ts"]
}

Bundling details

Bundled with Angular CLI

Preact w/Class

Dependencies

preact : ^10.3.4
@wcd/preact-custom-element : ^3.1.2

Component size including library

Minified - Uncompressed 11,232
Minified + Gzip 4,632
Minified + Brotli 4,210

Composition

Open Bundle Visualizer

Compilation details

All Sources have been Transpiled by WebComponents.dev compiler with the following Babel settings:

[
  [
    Babel.availablePresets['env'],
    {
      targets: {
        browsers:
          'last 2 Chrome versions, last 2 Firefox versions, last 2 Safari versions, last 2 iOS versions, last 2 Android versions',
      },
      modules: 'false',
      bugfixes: true,
    },
  ],
  [Babel.availablePresets['react']],
],
plugins: [
  [Babel.availablePlugins['proposal-decorators'], { legacy: true }],
  [Babel.availablePlugins['proposal-class-properties'], { loose: true }],
]

Bundling details

Bundled with Rollup

import commonjs from '@rollup/plugin-commonjs';
import resolve from '@rollup/plugin-node-resolve';
import replace from '@rollup/plugin-replace';
import { terser } from 'rollup-plugin-terser';

...

    input: 'source.js',
    output: {
      dir: 'dist'),
      format: 'esm',
    },
    plugins: [
      replace({
        'process.env.NODE_ENV': JSON.stringify('production'),
      }),
      withdeps ? resolve() : undefined,
      commonjs( /* { namedExports: for React } */ ),
      terser({ output: { comments: false } }),

...

React w/Class

Dependencies

react : ^16.13.1
react-dom : ^16.13.1
react-to-webcomponent : ^1.4.0

Component size including library

Minified - Uncompressed 131,652
Minified + Gzip 40,937
Minified + Brotli 36,080

Composition

Open Bundle Visualizer

Compilation details

All Sources have been Transpiled by WebComponents.dev compiler with the following TypeScript settings:
 {
  module: ts.ModuleKind.ESNext,
  experimentalDecorators: true,
  emitDecoratorMetadata: true,
  lib: ['ESNext', 'dom'],
  target: ts.ScriptTarget.ESNext
};

Bundling details

Bundled with Rollup

import commonjs from '@rollup/plugin-commonjs';
import resolve from '@rollup/plugin-node-resolve';
import replace from '@rollup/plugin-replace';
import { terser } from 'rollup-plugin-terser';

...

    input: 'source.js',
    output: {
      dir: 'dist'),
      format: 'esm',
    },
    plugins: [
      replace({
        'process.env.NODE_ENV': JSON.stringify('production'),
      }),
      withdeps ? resolve() : undefined,
      commonjs( /* { namedExports: for React } */ ),
      terser({ output: { comments: false } }),

...

React w/Hook

Dependencies

react : ^16.13.1
react-dom : ^16.13.1
react-to-webcomponent : ^1.4.0

Component size including library

Minified - Uncompressed 131,523
Minified + Gzip 40,907
Minified + Brotli 36,034

Composition

Open Bundle Visualizer

Compilation details

All Sources have been Transpiled by WebComponents.dev compiler with the following TypeScript settings:
 {
  module: ts.ModuleKind.ESNext,
  experimentalDecorators: true,
  emitDecoratorMetadata: true,
  lib: ['ESNext', 'dom'],
  target: ts.ScriptTarget.ESNext
};

Bundling details

Bundled with Rollup

import commonjs from '@rollup/plugin-commonjs';
import resolve from '@rollup/plugin-node-resolve';
import replace from '@rollup/plugin-replace';
import { terser } from 'rollup-plugin-terser';

...

    input: 'source.js',
    output: {
      dir: 'dist'),
      format: 'esm',
    },
    plugins: [
      replace({
        'process.env.NODE_ENV': JSON.stringify('production'),
      }),
      withdeps ? resolve() : undefined,
      commonjs( /* { namedExports: for React } */ ),
      terser({ output: { comments: false } }),

...

Riot

Dependencies

riot : ^4.11.1
@riotjs/custom-elements : ^4.1.1

Component size including library

Minified - Uncompressed 18,576
Minified + Gzip 6,799
Minified + Brotli 6,149

Composition

Open Bundle Visualizer

Compilation details

All Sources have been Transpiled by WebComponents.dev compiler with the following Riot settings:

const settings = {
  scopedCss: false,
};

Bundling details

Bundled with Rollup

import commonjs from '@rollup/plugin-commonjs';
import resolve from '@rollup/plugin-node-resolve';
import replace from '@rollup/plugin-replace';
import { terser } from 'rollup-plugin-terser';

...

    input: 'source.js',
    output: {
      dir: 'dist'),
      format: 'esm',
    },
    plugins: [
      replace({
        'process.env.NODE_ENV': JSON.stringify('production'),
      }),
      withdeps ? resolve() : undefined,
      commonjs( /* { namedExports: for React } */ ),
      terser({ output: { comments: false } }),

...

Svelte

Dependencies

svelte : ^3.20.1

Component size including library

Minified - Uncompressed 3,578
Minified + Gzip 1,718
Minified + Brotli 1,500

Composition

Open Bundle Visualizer

Compilation details

All Sources have been Transpiled by WebComponents.dev compiler with the following Svelte settings:

const options = {
  format: 'esm',
  customElement: true
};

Bundling details

Bundled with Rollup

import commonjs from '@rollup/plugin-commonjs';
import resolve from '@rollup/plugin-node-resolve';
import replace from '@rollup/plugin-replace';
import { terser } from 'rollup-plugin-terser';

...

    input: 'source.js',
    output: {
      dir: 'dist'),
      format: 'esm',
    },
    plugins: [
      replace({
        'process.env.NODE_ENV': JSON.stringify('production'),
      }),
      withdeps ? resolve() : undefined,
      commonjs( /* { namedExports: for React } */ ),
      terser({ output: { comments: false } }),

...

Vue.js

Dependencies

vue : ^2.6.11
vue-custom-element : ^3.2.14

Component size including library

Minified - Uncompressed 74,862
Minified + Gzip 26,389
Minified + Brotli 23,690

Composition

Open Bundle Visualizer

Compilation details

All Sources have been Transpiled by WebComponents.dev compiler using @webcomponents-dev/vue-sfc2esm

Bundling details

Bundled with Rollup

import commonjs from '@rollup/plugin-commonjs';
import resolve from '@rollup/plugin-node-resolve';
import replace from '@rollup/plugin-replace';
import { terser } from 'rollup-plugin-terser';

...

    input: 'source.js',
    output: {
      dir: 'dist'),
      format: 'esm',
    },
    plugins: [
      replace({
        'process.env.NODE_ENV': JSON.stringify('production'),
      }),
      withdeps ? resolve() : undefined,
      commonjs( /* { namedExports: for React } */ ),
      terser({ output: { comments: false } }),

...

Bundle Size Comparison

Bundle size of 1 component

If you only deliver a single <my-counter/> you have the full cost of the library for a single component. This is the total cost of a single component with the library included.

Estimated Bundle size of 30 components using the same library

This is an estimated size of a bundle of 30 my-counter-like components using the same library. All components will share the library code so the estimated size is calculated with: 1 bundle-with-dependencies + 29x components-without-dependencies.

Performance

First Contentful Paint

The First Contentful Paint is benchmarked with Tachometer.

The benchmark page is composed of 50 <my-counter></my-counter> laid out in a single page with the library bundle code inlined.

Notes

Take into consideration that there is a margin of error of 2-3ms.

Things that play a role in this performance figure are:

  • The size of the JavaScript to parse (bigger = longer).
  • The performance of the library to create the DOM nodes from scratch.

Everything runs locally so the network download time is not taken into account here.

Everything runs on a relatively powerful laptop and results could be completely different on a lower-end device like a phone or a tablet.

Unfortunately, for technical reasons, we couln't include

  • Angular 9 Elements
  • Stencil
  • Vue.js

Stay tuned for the next update!

Final Notes

It's hard to find so much diversity in a technical solution. It tells a lot about Web Components and the low level API provided. All the 30 variants could be on the same page if we wanted to!

On bundle size, the results are already very promising but there is still room for improvement:

  • Some libraries would benefit from splitting their features, to better benefit from tree-shaking.
  • Except Stencil and Svelte, none of the libraries offer CSS minification out of the box.

Web Components are well supported in browsers now. Still, for old browsers (including IE11), polyfills are required. The bundle sizes reported here are only for modern browsers that don't need any polyfills. It was too much additional work to include polyfills in this post. Maybe later... before they become obsolete.

We will report on new libraries and updates. Make sure to follow us on Twitter.

Feedback or Questions

Please chat with us on our Discord or Twitter channels.

Thanks

Thanks to all those who helped us review and correct this analysis.

Changes since March release

  • Upgrade of the full compilation pipeline - including Babel 7.9 + bugfixes option
  • Terser remove all comments: terser({ output: { comments: false } })
  • Added uhtml implementation
  • Added uce implementation
  • Added CanJS implementation
  • Added Solid Element implementation
  • Added Angular 9 Elements implementation
  • Graphs are made with Charts.js now
  • Updated graphs with stacked component/library sizes
  • Overall simplified content
  • Performance chapter

Brought to you by Twitter Discord