It is often said that web components are slow. This was also my experience when I first tried building web components a few years ago. At the time I was using the Stencil framework, because I didn't feel confident that they could be built well without a framework. Performance when putting hundreds of them on a page was so bad that I ended up going back to React.
But as I've gotten deeper into vanilla web development I started to realize that maybe I was just using it wrong. Perhaps web components can be fast, if built in a light-weight way. This article is an attempt to settle the question "How fast are web components?".
The lay of the land
What kinds of questions did I want answered?
- How many web components can you render on the page in a millisecond?
- Does the technique used to built the web component matter, which technique is fastest?
- How do web component frameworks compare? I used Lit as the framework of choice as it is well-respected.
- How does React compare?
- What happens when you combine React with web components?
To figure out the answer I made a benchmark as a vanilla web page (of course),
that renders thousands of very simple components containing only <span>.</span>
and measured the elapsed time. This benchmark was then run on multiple devices and multiple browsers
to figure out performance characteristics. The ultimate goal of this test is to figure out the absolute best performance
that can be extracted from the most minimal web component.
To get a performance range I used two devices for testing:
- A Macbook Air M1 running MacOS, to stand in as the "fast" device, comparable to a new high end iPhone.
- An Asus Chi T300 Core M from 2015 running Linux Mint Cinnamon, to stand in as the "slow" device, comparable to an older low end Android.
Between these devices is a 7x CPU performance gap.
The test
The test is simple: render thousands of components using a specific technique,
call requestAnimationFrame()
repeatedly until they actually render,
then measure elapsed time. This produces a components per millisecond number.
The techniques being compared:
- innerHTML: each web component renders its content by assigning to
this.innerHTML
- append: each web component creates the span using
document.createElement
and then appends it to itself - append (buffered): same as the append method, except all web components are first buffered to a document fragment which is then appended to the DOM
- shadow + innerHTML: the same as innerHTML, except each component has a shadow DOM
- shadow + append: the same as append, except each component has a shadow DOM
- template + append: each web component renders its content by cloning a template and appending it
- textcontent: each web component directly sets its textContent property, instead of adding a span (making the component itself be the span)
- direct: appends spans instead of custom elements, to be able to measure custom element overhead
- lit: each web component is rendered using the lit framework, in the way that its documentation recommends
- react pure: rendering in React as a standard React component, to have a baseline for comparison to mainstream web development
- react + wc: each React component wraps the append-style web component
- (norender): same as other strategies, except the component is only created but not added to the DOM, to separate out component construction cost
This test was run on M1 in Brave, Chrome, Edge, Firefox and Safari. And on Chi in Chrome and Firefox. It was run for 10 iterations and a geometric mean was taken of the results.
The results
First, let's compare techniques. The number here is components per millisecond, so higher is better.
Author's note: the numbers from the previous version of this article are crossed out.
Chrome on M1 | |
---|---|
technique | components/ms |
innerHTML | |
append | |
append (buffered) | |
shadow + innerHTML | |
shadow + append | |
template + append | |
textcontent | 345 |
direct | 461 |
lit | |
react pure | |
react + wc | |
append (norender) | 1393 |
shadow (norender) | 814 |
direct (norender) | 4277 |
lit (norender) | 880 |
Chrome on Chi, best of three | |
---|---|
technique | components/ms |
innerHTML | |
append | |
append (buffered) | |
shadow + innerHTML | |
shadow + append | |
template + append | |
textcontent | 81 |
direct | 116 |
lit | |
react pure | |
react + wc | |
append (norender) | 434 |
shadow (norender) | 231 |
direct (norender) | 1290 |
lit (norender) | 239 |
One relief right off the bat is that even the slowest implementation on the slow device renders 100.000 components in 4 seconds. React is roughly in the same performance class as well-written web components. That means for a typical web app performance is not a reason to avoid web components.
As far as web component technique goes, the performance delta between the fastest and the slowest technique is around 2x, so again for a typical web app that difference will not matter. Things that slow down web components are shadow DOM and innerHTML. Appending directly created elements or cloned templates and avoiding shadow DOM is the right strategy for a well-performing web component that needs to end up on the page thousands of times.
On the slow device the Lit framework is a weak performer, probably due to its use of shadow DOM and JS-heavy approaches. Meanwhile, pure React is the best performer, because while it does more work in creating the virtual DOM and diffing it to the real DOM, it benefits from not having to initialize the web component class instances. Consequently, when wrapping web components inside React components we see React's performance advantage disappear, and that it adds a performance tax. In the grand scheme of things however, the differences between React and optimized web components remains small.
The fast device is up to 5x faster than the slow device in Chrome, depending on the technique used, so it is really worth testing applications on slow devices to get an idea of the range of performance.
Next, let's compare browsers:
M1, append, best of three | |
---|---|
browser | components/ms |
Brave | |
Chrome | |
Edge | |
Firefox | |
Safari |
Chi, append, best of three | |
---|---|
browser | components/ms |
Chrome | |
Firefox |
Brave is really slow, probably because of its built-in ad blocking. Ad blocking extensions also slow down the other browsers by a lot. Safari, Chrome and Edge end up in roughly the same performance bucket. Firefox is the best performer overall. Using the "wrong" browser can halve the performance of a machine.
Author's note: due to a measurement error in measuring elapsed time, the previous version of this article had Safari as fastest and Firefox as middle of the pack.
There is a large performance gap when you compare the slowest technique on the slowest browser on the slowest device, with its fastest opposite combo. Specifically, there is a 16x performance gap:
- textContent, Firefox on M1: 430 components/ms
- Shadow DOM + innerHTML, Chrome on Chi: 26 components/ms
That means it becomes worthwhile to carefully consider technique when having to support a wide range of browsers and devices, because a bad combination may lead to a meaningfully degraded user experience. And of course, you should always test your web app on a slow device to make sure it still works ok.
Bottom line
I feel confident now that web components can be fast enough for almost all use cases where someone might consider React instead.
However, it does matter how they are built. Shadow DOM should not be used for smaller often used web components, and the contents of those smaller components should be built using append operations instead of innerHTML. The use of web component frameworks might impact their performance significantly, and given how easy it is to write vanilla web components I personally don't see the point behind Lit or Stencil. YMMV.
The full benchmark code and results can be found on Github.