Explore 25 ways to make a Web Component. Compare coding styles and bundle sizes.
DEPRECATED: Latest version available here

Introduction

The same <my-counter/> Web Component

has been written in the following 25 variants:

HTMLElement

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

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

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

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

LitElement

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

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

Neow (Alpha)

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

Homepage: https://neow.dev

GitHub: (alpha)

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

SkateJS (with Lit-html)

SkateJS - Effortless custom elements powered by modern view libraries.

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

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

SkateJS (with Preact)

SkateJS - Effortless custom elements powered by modern view libraries.

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

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

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

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

Haunted

React's Hooks API implemented for web components 👻

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

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

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

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

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

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

Lightning Web Components

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

Homepage: https://lwc.dev

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

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

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

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/

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/

Riot (wrapped with @riotjs/custom-elements)

Simple and elegant component-based UI library

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

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

Svelte (with {customElement: true})

Cybernetically enhanced web apps

Homepage: https://svelte.dev/

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

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

All constructed with similar html template...

<button onclick="dec">-</button>
<span></span>
<button onclick="inc">+</button>
... and Cascading Style Sheet
* {
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;
}

Source Code style Analysis

HTMLElement

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)

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)

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);

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

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)

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

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)

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)

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

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

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

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

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

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

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

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

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--;
}
}
});

Lightning Web Components

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));

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>
);
}
}

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

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)

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)

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)

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})

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)

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>

Source Code Size Comparison

Notes

Source code size isn't extremely important or relevant. Sometimes writing something that is - easy to read and understand - is longer.

Still, writing a Web Component in bare HTMLElement is the most verbose by quite a margin. It's probably more productive to use libraries.

Bundle Analysis

HTMLElement

No dependencies

Component size including library

Minified - Uncompressed1,293
Minified + Gzip558
Minified + Brotli418

Composition

Open Bundle Visualizer

Compilation details

All Sources have been Transpiled by WebComponents.dev compiler with the following Babel settings:
const presets = [
[Babel.availablePresets['stage-2'], { decoratorsLegacy: true }],
[Babel.availablePresets['react']],
];

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(),

...

HTMLElement (w/ Lighterhtml)

Dependencies

lighterhtml : ^2.0.9

Component size including library

Minified - Uncompressed15,533
Minified + Gzip6,635
Minified + Brotli6,005

Composition

Open Bundle Visualizer

Compilation details

All Sources have been Transpiled by WebComponents.dev compiler with the following Babel settings:
const presets = [
[Babel.availablePresets['stage-2'], { decoratorsLegacy: true }],
[Babel.availablePresets['react']],
];

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(),

...

HTMLElement (w/ Lit-html)

Dependencies

lit-html : ^1.1.2

Component size including library

Minified - Uncompressed14,191
Minified + Gzip3,832
Minified + Brotli3,341

Composition

Open Bundle Visualizer

Compilation details

All Sources have been Transpiled by WebComponents.dev compiler with the following Babel settings:
const presets = [
[Babel.availablePresets['stage-2'], { decoratorsLegacy: true }],
[Babel.availablePresets['react']],
];

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(),

...

HyperHTML-Element

Dependencies

hyperhtml-element : ^3.12.2

Component size including library

Minified - Uncompressed22,970
Minified + Gzip9,120
Minified + Brotli8,299

Composition

Open Bundle Visualizer

Compilation details

All Sources have been Transpiled by WebComponents.dev compiler with the following Babel settings:
const presets = [
[Babel.availablePresets['stage-2'], { decoratorsLegacy: true }],
[Babel.availablePresets['react']],
];

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(),

...

LitElement

Dependencies

lit-element : ^2.2.1

Component size including library

Minified - Uncompressed26,930
Minified + Gzip7,149
Minified + Brotli6,335

Composition

Open Bundle Visualizer

Compilation details

All Sources have been Transpiled by WebComponents.dev compiler with the following Babel settings:
const presets = [
[Babel.availablePresets['stage-2'], { decoratorsLegacy: true }],
[Babel.availablePresets['react']],
];

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(),

...

Neow (Alpha)

Dependencies

@neow/core : 0.0.6

Component size including library

Minified - Uncompressed4,565
Minified + Gzip1,939
Minified + Brotli1,666

Composition

Open Bundle Visualizer

Compilation details

All Sources have been Transpiled by WebComponents.dev compiler with the following Babel settings:
const presets = [
[Babel.availablePresets['stage-2'], { decoratorsLegacy: true }],
[Babel.availablePresets['react']],
];

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(),

...

Omi w/Class

Dependencies

omi : ^6.17.0

Component size including library

Minified - Uncompressed25,354
Minified + Gzip8,724
Minified + Brotli7,808

Composition

Open Bundle Visualizer

Compilation details

All Sources have been Transpiled by WebComponents.dev compiler with the following Babel settings:
const presets = [
[Babel.availablePresets['stage-2'], { decoratorsLegacy: true }],
[Babel.availablePresets['react']],
];

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(),

...

SkateJS (with Lit-html)

Dependencies

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

Component size including library

Minified - Uncompressed19,788
Minified + Gzip5,529
Minified + Brotli4,869

Composition

Open Bundle Visualizer

Compilation details

All Sources have been Transpiled by WebComponents.dev compiler with the following Babel settings:
const presets = [
[Babel.availablePresets['stage-2'], { decoratorsLegacy: true }],
[Babel.availablePresets['react']],
];

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(),

...

SkateJS (with Preact)

Dependencies

@skatejs/element-preact : 0.0.1

Component size including library

Minified - Uncompressed17,174
Minified + Gzip6,046
Minified + Brotli5,486

Composition

Open Bundle Visualizer

Compilation details

All Sources have been Transpiled by WebComponents.dev compiler with the following Babel settings:
const presets = [
[Babel.availablePresets['stage-2'], { decoratorsLegacy: true }],
[Babel.availablePresets['react']],
];

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(),

...

SlimJS

Dependencies

slim-js : ^4.0.7

Component size including library

Minified - Uncompressed9,467
Minified + Gzip3,661
Minified + Brotli3,220

Composition

Open Bundle Visualizer

Compilation details

All Sources have been Transpiled by WebComponents.dev compiler with the following Babel settings:
const presets = [
[Babel.availablePresets['stage-2'], { decoratorsLegacy: true }],
[Babel.availablePresets['react']],
];

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(),

...

Atomico

Dependencies

atomico : ^0.19.0

Component size including library

Minified - Uncompressed8,148
Minified + Gzip3,698
Minified + Brotli3,294

Composition

Open Bundle Visualizer

Compilation details

All Sources have been Transpiled by WebComponents.dev compiler with the following Babel settings:
const presets = [
[Babel.availablePresets['stage-2'], { decoratorsLegacy: true }],
[Babel.availablePresets['react']],
];

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(),

...

Haunted

Dependencies

haunted : ^4.7.0

Component size including library

Minified - Uncompressed19,563
Minified + Gzip5,586
Minified + Brotli4,918

Composition

Open Bundle Visualizer

Compilation details

All Sources have been Transpiled by WebComponents.dev compiler with the following Babel settings:
const presets = [
[Babel.availablePresets['stage-2'], { decoratorsLegacy: true }],
[Babel.availablePresets['react']],
];

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(),

...

Heresy w/Hook

Dependencies

heresy : ^0.24.4

Component size including library

Minified - Uncompressed24,835
Minified + Gzip10,098
Minified + Brotli9,216

Composition

Open Bundle Visualizer

Compilation details

All Sources have been Transpiled by WebComponents.dev compiler with the following Babel settings:
const presets = [
[Babel.availablePresets['stage-2'], { decoratorsLegacy: true }],
[Babel.availablePresets['react']],
];

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(),

...

Heresy

Dependencies

heresy : ^0.24.4

Component size including library

Minified - Uncompressed24,958
Minified + Gzip10,139
Minified + Brotli9,254

Composition

Open Bundle Visualizer

Compilation details

All Sources have been Transpiled by WebComponents.dev compiler with the following Babel settings:
const presets = [
[Babel.availablePresets['stage-2'], { decoratorsLegacy: true }],
[Babel.availablePresets['react']],
];

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(),

...

Hybrids

Dependencies

hybrids : ^4.1.3

Component size including library

Minified - Uncompressed19,362
Minified + Gzip6,429
Minified + Brotli5,786

Composition

Open Bundle Visualizer

Compilation details

All Sources have been Transpiled by WebComponents.dev compiler with the following Babel settings:
const presets = [
[Babel.availablePresets['stage-2'], { decoratorsLegacy: true }],
[Babel.availablePresets['react']],
];

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(),

...

Litedom

Dependencies

litedom : ^0.12.1

Component size including library

Minified - Uncompressed9,411
Minified + Gzip3,847
Minified + Brotli3,422

Composition

Open Bundle Visualizer

Compilation details

All Sources have been Transpiled by WebComponents.dev compiler with the following Babel settings:
const presets = [
[Babel.availablePresets['stage-2'], { decoratorsLegacy: true }],
[Babel.availablePresets['react']],
];

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(),

...

Ottavino

Dependencies

ottavino : ^0.2.4

Component size including library

Minified - Uncompressed4,446
Minified + Gzip1,985
Minified + Brotli1,717

Composition

Open Bundle Visualizer

Compilation details

All Sources have been Transpiled by WebComponents.dev compiler with the following Babel settings:
const presets = [
[Babel.availablePresets['stage-2'], { decoratorsLegacy: true }],
[Babel.availablePresets['react']],
];

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(),

...

Lightning Web Components

Dependencies

lwc : ^1.2.0

Component size including library

Minified - Uncompressed36,290
Minified + Gzip12,625
Minified + Brotli11,257

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(),

...

Stencil

Dependencies

@stencil/core : ^1.8.9

Component size including library

Minified - Uncompressed6,808
Minified + Gzip3,575
Minified + Brotli3,174

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

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

Dependencies

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

Component size including library

Minified - Uncompressed11,303
Minified + Gzip4,654
Minified + Brotli4,230

Composition

Open Bundle Visualizer

Compilation details

All Sources have been Transpiled by WebComponents.dev compiler with the following Babel settings:
const presets = [
[Babel.availablePresets['stage-2'], { decoratorsLegacy: true }],
[Babel.availablePresets['react']],
];

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(),

...

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

Dependencies

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

Component size including library

Minified - Uncompressed131,959
Minified + Gzip41,458
Minified + Brotli36,483

Composition

Open Bundle Visualizer

Compilation details

All Sources have been Transpiled by WebComponents.dev compiler with the following TypeScript settings:
const compilerOptions = {
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(),

...

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

Dependencies

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

Component size including library

Minified - Uncompressed131,831
Minified + Gzip41,419
Minified + Brotli36,438

Composition

Open Bundle Visualizer

Compilation details

All Sources have been Transpiled by WebComponents.dev compiler with the following TypeScript settings:
const compilerOptions = {
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(),

...

Riot (wrapped with @riotjs/custom-elements)

Dependencies

riot : ^4.9.0
@riotjs/custom-elements : ^4.1.0

Component size including library

Minified - Uncompressed18,418
Minified + Gzip6,767
Minified + Brotli6,113

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(),

...

Svelte (with {customElement: true})

Dependencies

svelte : ^3.18.2

Component size including library

Minified - Uncompressed3,535
Minified + Gzip1,698
Minified + Brotli1,482

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(),

...

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

Dependencies

vue : ^2.6.11
vue-custom-element : ^3.2.12

Component size including library

Minified - Uncompressed75,044
Minified + Gzip26,550
Minified + Brotli23,785

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(),

...

Bundle Size Comparison

Bundle size of 1 component without library bundled

This is the cost to expect for every new my-counter-like component added to the same page. The library isn't included.

Notes

When using a library (versus bare HTMLElement without dependencies), a lot is managed by the library, leading to tiny component fragments.

Hybrids, Heresy w/hooks and Ottavino are doing particularly well here.

HTMLElement isn't the worst. Compilers like LWC and Svelte create bigger HTMLElement classes than "written by hand".

Bundle size of 1 component with library bundled

If you only deliver a single Web Component 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.

Notes

Adding the cost of the library paint a complete different picture.

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.

On the other hand, 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.

Estimated Bundle size of the library at runtime

This is an estimate of the bundle size of the library only. This cost is only payed once accross all Web Components using the same library.

Notes

Don't get into conclusions too quickly. 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 component using the broad spectrum of the library capabilities, you endup with the entire library anyway.

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 bundles-without-dependencies.

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 25 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 Spectrum or Twitter channels.

Thanks

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

Updates

Feb 23rd 2020

Following comments from @justinfagnani the styles and the templates have been simplified to a form more idiomatic to ShadowDOM usage. See updated Introduction.

Also:

  • Litedom, Ottavino and hyperHTML are now properly ShadowDOM enabled
  • Renamed Lit-Element => LitElement
  • LitElement source fixed to use properties and no update()
  • Removed 6 chars comment on SlimJS
  • Removed id attributes on HTMLElement w/ LighterHTML


Brought to you by
Follow us