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
HTMLElement (w/ Lit-html)
Lit-Html - An efficient, expressive, extensible HTML templating library for JavaScript
Homepage: https://lit-html.polymer-project.org/
HyperHTML-Element
Framework agnostic, hyperHTML can be used to render any view, including Custom Elements and Web Components.
LitElement
Homepage: https://lit-element.polymer-project.org/
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/
SkateJS (with Preact)
SkateJS - Effortless custom elements powered by modern view libraries.
Homepage: https://skatejs.netlify.com/
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
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/
Haunted
React's Hooks API implemented for web components 👻
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
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
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
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/
Ottavino
Tiny, Fast and Declarative User Interface Development. Using native custom elements API (but not only). As simple as it gets.
Lightning Web Components
⚡️ LWC - A Blazing Fast, Enterprise-Grade Web Components Foundation
Homepage: https://lwc.dev
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/
Preact w/Class (wrapped with preact-custom-element)
Fast 3kB alternative to React with the same modern API.
Homepage: https://preactjs.com/
React w/Class (wrapped with react-to-webcomponent)
A JavaScript library for building user interfaces
Homepage: https://reactjs.org/
React w/Hook (wrapped with react-to-webcomponent)
A JavaScript library for building user interfaces
Homepage: https://reactjs.org/
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/
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.devclass 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.devimport { 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.devimport { 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.devimport 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.devimport { 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.devimport { 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.devimport { 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.devimport 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.devimport { 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.devimport { 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.devimport { 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.devimport { 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.devimport { 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.devimport 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.devimport { 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.devimport { 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.devimport { 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.devimport 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.devimport 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 - Uncompressed | 1,293 |
Minified + Gzip | 558 |
Minified + Brotli | 418 |
Composition
Open Bundle VisualizerCompilation 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 Rollupimport 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 - Uncompressed | 15,533 |
Minified + Gzip | 6,635 |
Minified + Brotli | 6,005 |
Composition
Open Bundle VisualizerCompilation 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 Rollupimport 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 - Uncompressed | 14,191 |
Minified + Gzip | 3,832 |
Minified + Brotli | 3,341 |
Composition
Open Bundle VisualizerCompilation 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 Rollupimport 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 - Uncompressed | 22,970 |
Minified + Gzip | 9,120 |
Minified + Brotli | 8,299 |
Composition
Open Bundle VisualizerCompilation 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 Rollupimport 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 - Uncompressed | 26,930 |
Minified + Gzip | 7,149 |
Minified + Brotli | 6,335 |
Composition
Open Bundle VisualizerCompilation 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 Rollupimport 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 - Uncompressed | 4,565 |
Minified + Gzip | 1,939 |
Minified + Brotli | 1,666 |
Composition
Open Bundle VisualizerCompilation 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 Rollupimport 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 - Uncompressed | 25,354 |
Minified + Gzip | 8,724 |
Minified + Brotli | 7,808 |
Composition
Open Bundle VisualizerCompilation 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 Rollupimport 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 - Uncompressed | 19,788 |
Minified + Gzip | 5,529 |
Minified + Brotli | 4,869 |
Composition
Open Bundle VisualizerCompilation 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 Rollupimport 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 - Uncompressed | 17,174 |
Minified + Gzip | 6,046 |
Minified + Brotli | 5,486 |
Composition
Open Bundle VisualizerCompilation 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 Rollupimport 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 - Uncompressed | 9,467 |
Minified + Gzip | 3,661 |
Minified + Brotli | 3,220 |
Composition
Open Bundle VisualizerCompilation 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 Rollupimport 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 - Uncompressed | 8,148 |
Minified + Gzip | 3,698 |
Minified + Brotli | 3,294 |
Composition
Open Bundle VisualizerCompilation 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 Rollupimport 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 - Uncompressed | 19,563 |
Minified + Gzip | 5,586 |
Minified + Brotli | 4,918 |
Composition
Open Bundle VisualizerCompilation 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 Rollupimport 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 - Uncompressed | 24,835 |
Minified + Gzip | 10,098 |
Minified + Brotli | 9,216 |
Composition
Open Bundle VisualizerCompilation 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 Rollupimport 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 - Uncompressed | 24,958 |
Minified + Gzip | 10,139 |
Minified + Brotli | 9,254 |
Composition
Open Bundle VisualizerCompilation 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 Rollupimport 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 - Uncompressed | 19,362 |
Minified + Gzip | 6,429 |
Minified + Brotli | 5,786 |
Composition
Open Bundle VisualizerCompilation 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 Rollupimport 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 - Uncompressed | 9,411 |
Minified + Gzip | 3,847 |
Minified + Brotli | 3,422 |
Composition
Open Bundle VisualizerCompilation 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 Rollupimport 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 - Uncompressed | 4,446 |
Minified + Gzip | 1,985 |
Minified + Brotli | 1,717 |
Composition
Open Bundle VisualizerCompilation 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 Rollupimport 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 - Uncompressed | 36,290 |
Minified + Gzip | 12,625 |
Minified + Brotli | 11,257 |
Composition
Open Bundle VisualizerCompilation 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 Rollupimport 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 - Uncompressed | 6,808 |
Minified + Gzip | 3,575 |
Minified + Brotli | 3,174 |
Composition
Open Bundle VisualizerCompilation 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 CLIPreact w/Class (wrapped with preact-custom-element)
Dependencies
preact : ^10.3.2
@wcd/preact-custom-element : ^3.1.2
Component size including library
Minified - Uncompressed | 11,303 |
Minified + Gzip | 4,654 |
Minified + Brotli | 4,230 |
Composition
Open Bundle VisualizerCompilation 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 Rollupimport 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 - Uncompressed | 131,959 |
Minified + Gzip | 41,458 |
Minified + Brotli | 36,483 |
Composition
Open Bundle VisualizerCompilation 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 Rollupimport 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 - Uncompressed | 131,831 |
Minified + Gzip | 41,419 |
Minified + Brotli | 36,438 |
Composition
Open Bundle VisualizerCompilation 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 Rollupimport 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 - Uncompressed | 18,418 |
Minified + Gzip | 6,767 |
Minified + Brotli | 6,113 |
Composition
Open Bundle VisualizerCompilation details
All Sources have been Transpiled by WebComponents.dev compiler with the following Riot settings:const settings = {
scopedCss: false,
};
Bundling details
Bundled with Rollupimport 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 - Uncompressed | 3,535 |
Minified + Gzip | 1,698 |
Minified + Brotli | 1,482 |
Composition
Open Bundle VisualizerCompilation 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 Rollupimport 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 - Uncompressed | 75,044 |
Minified + Gzip | 26,550 |
Minified + Brotli | 23,785 |
Composition
Open Bundle VisualizerCompilation details
All Sources have been Transpiled by WebComponents.dev compiler using @webcomponents-dev/vue-sfc2esmBundling details
Bundled with Rollupimport 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 Discord or Twitter channels.
Thanks
Thanks to all those who helped us review and correct this analysis.
- @Uppercod creator of Atomico
- @adamdbradley creator of Stencil
- @matthewcp creator of Haunted
- @eavichay creator of SlimJs, Ottavino and Neow (in alpha)
- @WebReflection creator of Heresy, HyperHTML and lighterhtml
- @smalluban creator of Hybrids
- @_developit creator of Preact
- @KevinJHill @diervo @pmdartus of the LWC team
- @Rich_Harris representing Svelte
- @justinfagnani from lit-html and LitElement team
- Tabs from @lion/tabs - Thanks @dakmor and ING
- Charts library from ApexCharts
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 Twitter Discord