Back to Home

Web components issues: performance and API

Article analyzes key web components issues: excessive boilerplate for properties, low performance due to host proxy, global registration with conflicts and Shadow DOM inconveniences. Benchmarks and code examples are provided.

Why web components lag: benchmarks and code
Advertisement 728x90

Web Component Performance and API Challenges

Creating a web component with just one property requires a significant amount of code using the Custom Elements API. The base implementation is limited to string attributes, forcing serialization for other data types.

Example of a minimal component:

class MyFoo extends HTMLElement {

	_bar = 0
	get bar() {
		return this._bar
	}
	set bar( next ) {
		this.setAttribute( 'bar', next )
	}
	
	static observedAttributes = [ 'bar' ]
	attributeChangedCallback( name, prev, next ) {
		this[ '_' + name ] = next
	}
	
}

globalThis.customElements.define( 'my-foo', MyFoo )

To support numbers, JSON serialization is added with a _muted_ flag to prevent recursion:

Google AdInline article slot
class MyFoo extends HTMLElement {

	_bar = 0
	get bar() {
		return this._bar
	}
	set bar( next ) {
		this._bar = next
		this._muted_ = true; try {
			this.setAttribute( 'bar', JSON.stringify( next ) )
		} finally { this._muted_ = false }
	}
	
	_muted_ = false
	static observedAttributes = [ 'bar' ]
	attributeChangedCallback( name, prev, next ) {
		if( this._muted_ ) return
		this[ '_' + name ] = JSON.parse( next )
	}
	
}

This introduces issues with attribute string escaping and demands additional abstraction layers for dates, objects, and other types.

Performance: Microseconds Kill Frames

Web components are orders of magnitude slower than regular JS objects due to runtime proxying. Comparison of creation time:

  • Regular JS object: ~0.01 μs
  • Web component: ~1–3 μs

A thousand components take 1–3 ms to create—eating up a substantial portion of the 16 ms frame budget. Property changes also require microseconds:

Google AdInline article slot
  • Simple JS assignment: nanoseconds
  • Web component setter: microseconds

The root cause is constant data exchange between JS and the browser’s C++ runtime, blocking JIT inlining. Memory usage: 124 bytes per trivial web component versus 16 bytes for a JS object.

Global Registration and Name Conflicts

Component registration is global and irreversible until the tab closes:

customElements.define( 'ya-button', YandexButton )
// ...
customElements.define( 'ya-button', YahooButton ) // 💥 Error: name already used

Name conflicts break modularity. While the CustomElementRegistry in Shadow DOM helps partially, polyfills aren’t available, and merging registries requires namespace management.

Google AdInline article slot

Alternatives:

  • A global fractal registry like DNS
  • VerLess — updates without breaking compatibility

Lifecycle Hooks and Shadow DOM

connectedCallback/disconnectedCallback fire on any component movement—even when moving to the same location—triggering cascades across subtrees. This leads to unnecessary async tasks.

Shadow DOM provides isolation but complicates styling: adoptedStyleSheets require dynamic imports of styles from outside. Element names must be strictly kebab-case, clashing with standard DOM elements.

What Matters

  • Performance: Web components are 100–1000x slower than JS objects due to host proxying
  • Boilerplate: Serialization/deserialization is mandatory for non-string properties
  • Registration: Global, irreversible, prone to name collisions
  • Lifecycle: Hooks trigger on moves, causing cascades
  • Shadow DOM: Complicates style customization and polyfilling

Web components don’t support TypeScript static typing or built-in reactivity, requiring frameworks for real-world apps.

— Editorial Team

Advertisement 728x90

Read Next