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:
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:
- 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.
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
No comments yet.