Compare coding style, bundle size and performance of 33 different ways to make a Web Component.
The same <my-counter/>
Web Component has been written in 33 variants.
HTMLElement
Homepage: https://html.spec.whatwg.org/multipage/custom-elements.html
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
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
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
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
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
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
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)
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
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
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
/** @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
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
/** @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));

Gallop
Use full power of web components
GitHub: https://github.com/tarnishablec/gallop
import { component, html, useState, useStyle, css } from "@gallop/gallop";
export const MyCounter = component(
"my-counter",
() => {
const [state] = useState({ count: 0 });
useStyle(
() => 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;
}
`
);
return html`
<button @click="${() => state.count--}">
-
</button>
<span>${state.count}</span>
<button @click="${() => state.count++}">
+
</button>
`;
},
{ propList: [] }
);
Haunted
React’s Hooks API implemented for web components ?
GitHub: https://github.com/matthewp/haunted
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
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
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
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
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
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--;
}
}
});
Swiss
Functional custom elements
GitHub: https://github.com/luwes/swiss
import { define } from "swiss";
import { html, render } from "lit-html";
const Counter = (CE) => (el) => {
el.attachShadow({ mode: "open" });
return {
update: () =>
render(
html`
<style>
* {
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;
}
</style>
<button @click="${() => el.count--}">-</button>
<span>${el.count}</span>
<button @click="${() => el.count++}">+</button>
`,
el.shadowRoot
),
};
};
define("my-counter", {
props: { count: 0 },
setup: Counter,
});
uce
µhtml based Custom Elements.
Homepage: https://github.com/WebReflection/uce
GitHub: https://github.com/WebReflection/uce
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
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));
Lume Element
Create Custom Elements with reactivity and automatic re-rendering
GitHub: https://github.com/lume/element
import { Element, css, createSignal } from "@lume/element";
export class MyCounter extends Element {
constructor() {
super();
this._count = createSignal(0);
}
get count() {
return this._count[0]();
}
set count(val) {
this._count[1](val);
}
template() {
return (
<>
<button onclick={() => (this.count -= 1)}>-</button>
<span>{this.count}</span>
<button onclick={() => (this.count += 1)}>+</button>
</>
);
}
static get css() {
return css`
* {
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;
}
`;
}
}
customElements.define("my-counter", MyCounter);
Solid Element
The Deceptively Simple User Interface Library
GitHub: https://github.com/ryansolid/solid
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
/* @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
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
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/
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/
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
<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
<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
<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>
HTMLElement
No dependencies
Component size including library
Minified – Uncompressed | 1,293 |
Minified + Gzip | 558 |
Minified + Brotli | 418 |
Composition
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.1.3
Component size including library
Minified – Uncompressed | 15,699 |
Minified + Gzip | 6,360 |
Minified + Brotli | 5,799 |
Composition
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,955 |
Minified + Gzip | 3,530 |
Minified + Brotli | 3,144 |
Composition
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.11.6
Component size including library
Minified – Uncompressed | 6,973 |
Minified + Gzip | 3,179 |
Minified + Brotli | 2,849 |
Composition
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,634 |
Minified + Gzip | 63,154 |
Minified + Brotli | 54,620 |
Composition
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 | 23,316 |
Minified + Gzip | 9,061 |
Minified + Brotli | 8,277 |
Composition
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,395 |
Minified + Gzip | 6,682 |
Minified + Brotli | 5,997 |
Composition
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
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.19.3
Component size including library
Minified – Uncompressed | 24,622 |
Minified + Gzip | 8,412 |
Minified + Brotli | 7,566 |
Composition
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,580 |
Minified + Gzip | 5,248 |
Minified + Brotli | 4,662 |
Composition
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,557 |
Minified + Gzip | 6,244 |
Minified + Brotli | 5,630 |
Composition
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
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.23.2
Component size including library
Minified – Uncompressed | 5,800 |
Minified + Gzip | 2,735 |
Minified + Brotli | 2,434 |
Composition
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 } }),
...
Gallop
Dependencies
@gallop/gallop : ^0.9.9-beta.6
Component size including library
Minified – Uncompressed | 11,921 |
Minified + Gzip | 4,581 |
Minified + Brotli | 4,083 |
Composition
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,327 |
Minified + Gzip | 5,267 |
Minified + Brotli | 4,708 |
Composition
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.28.1
Component size including library
Minified – Uncompressed | 26,556 |
Minified + Gzip | 9,984 |
Minified + Brotli | 9,094 |
Composition
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.28.1
Component size including library
Minified – Uncompressed | 26,679 |
Minified + Gzip | 10,022 |
Minified + Brotli | 9,130 |
Composition
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.2.1
Component size including library
Minified – Uncompressed | 20,811 |
Minified + Gzip | 6,941 |
Minified + Brotli | 6,257 |
Composition
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,392 |
Minified + Gzip | 3,835 |
Minified + Brotli | 3,412 |
Composition
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,444 |
Minified + Gzip | 1,977 |
Minified + Brotli | 1,709 |
Composition
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 } }),
...
Swiss
Dependencies
swiss : ^2.0.0
Component size including library
Minified – Uncompressed | 12,126 |
Minified + Gzip | 4,373 |
Minified + Brotli | 3,888 |
Composition
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 : ^0.13.1
Component size including library
Minified – Uncompressed | 8,563 |
Minified + Gzip | 3,829 |
Minified + Brotli | 3,438 |
Composition
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.6.2
Component size including library
Minified – Uncompressed | 38,531 |
Minified + Gzip | 13,079 |
Minified + Brotli | 11,763 |
Composition
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 } }),
...
Lume Element
Dependencies
@lume/element : 0.0.15
Component size including library
Minified – Uncompressed | 9,520 |
Minified + Gzip | 3,781 |
Minified + Brotli | 3,379 |
Composition
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 } }),
...
Solid Element
Dependencies
solid-js : ^0.18.4
solid-element : ^0.18.4
Component size including library
Minified – Uncompressed | 12,575 |
Minified + Gzip | 4,846 |
Minified + Brotli | 4,316 |
Composition
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.14.0
Component size including library
Minified – Uncompressed | 6,811 |
Minified + Gzip | 3,565 |
Minified + Brotli | 3,154 |
Composition
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.1.9
@angular/cli : ^9.1.7
@angular/common : ^9.1.9
@angular/compiler : ^9.1.9
@angular/compiler-cli : ^9.1.9
@angular-devkit/build-angular : ^0.901.7
document-register-element : ^1.14.3
@angular/elements : ^9.1.9
@angular/platform-browser-dynamic : ^9.1.9
@angular/platform-browser : ^9.1.9
Component size including library
Minified – Uncompressed | 227,232 |
Minified + Gzip | 69,831 |
Minified + Brotli | 59,961 |
Composition
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/009-angular-9-elements",
"sourceRoot": "download/009-angular-9-elements/src",
"projectType": "application",
"prefix": "app",
"schematics": {},
"architect": {
"build": {
"builder": "@angular-devkit/build-angular:browser",
"options": {
"aot": true,
"outputPath": "download/009-angular-9-elements/dist",
"index": "download/009-angular-9-elements/src/index.html",
"main": "download/009-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/009-angular-9-elements/dist/out-tsc",
"baseUrl": "download/009-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/009-angular-9-elements/src/index.ts", "polyfills.ts"]
}
Bundling details
Bundled with Angular CLI
Preact w/Class
Dependencies
preact : ^10.4.4
@wcd/preact-custom-element : ^3.1.3
Component size including library
Minified – Uncompressed | 11,617 |
Minified + Gzip | 4,774 |
Minified + Brotli | 4,346 |
Composition
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,430 |
Minified + Gzip | 40,901 |
Minified + Brotli | 36,008 |
Composition
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,306 |
Minified + Gzip | 40,870 |
Minified + Brotli | 36,004 |
Composition
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.12.4
@riotjs/custom-elements : ^4.1.1
Component size including library
Minified – Uncompressed | 18,809 |
Minified + Gzip | 6,906 |
Minified + Brotli | 6,240 |
Composition
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.23.0
Component size including library
Minified – Uncompressed | 3,592 |
Minified + Gzip | 1,728 |
Minified + Brotli | 1,511 |
Composition
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 | 76,860 |
Minified + Gzip | 27,176 |
Minified + Brotli | 24,357 |
Composition
Compilation details
Vue sources have been Transpiled by rollup-plugin-vue
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 } }),
...
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.
The bare HTMLElement component doesn’t have any dependency and creates a unmatched tiny package.
Svelte is doing also very well with a very small library runtime cost primarily due to excellent tree-shaking. This also means that as the component use more of Svelte capabilities, more of the library will be included.
Vue and React libraries do poorly on tree-shake so you pretty much have the total runtime, no matter how complex your components are. Vue 3 is supposed to offer better tree-shake support – it would be interesting to see.
Some libraries bring significant value and are more feature rich than others. It’s normal to see larger sizes in these cases.
Nevertheless, for a simple counter component, we would like to see more tree-shaking happening.
On the other hand, once you start having multiple components using a broad spectrum of the library capabilities, you end up with the entire library anyway.
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.
It’s interesting to see that some libraries managed to be smaller than bare HTMLElement. This is achieved by sharing more runtime accross the different components. Individual components are smaller and pay for the runtime. A library is not always an overhead !
On the opposite, Svelte has a super tiny runtime but the resulting components are bigger than most solutions. Only Vue 2 and Angular components are bigger.
The benchmark page is composed of 50 <my-counter></my-counter>
laid out in a single page with the library bundle code inlined.
Take into consideration that there is a margin of error of 2ms. So the first 16 libraries are moree of less equal!
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.
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 33 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:
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.