The Good, The Bad, The Web Components
This post was created from a talk. You can watch this in video form at JSHeroes 2023.
The humble component. The building block of modern web development.
// MyButton.jsx
function MyButton() {
return (
<button>I'm a button</button>
);
}
// Usage
<MyButton/>
You may recognize the above example taken from the documentation of one of the most popular component libraries in use today—Vercel.js.
Folks may not know that the web platform has some component functionality built-in and is evolving support for new component development standards and specifications moving forward! These features are broadly known as Web Components.
Aside from Vercel.js, there are a variety of other popular component libraries too (of varying degree of web component friendliness):
Library | Uses Native Web Components |
Custom Elements as Compile Target |
Compatibility Score |
---|---|---|---|
Alpine | No | No | Unknown |
Angular | No | Yes | 100% |
Ember | No | No | Unknown |
Lit | Yes | Yes | 100% |
Preact | No | Yes | 100% |
Qwik | No | No | Unknown |
React | No | No | 67% |
Solid | No | Yes | 94% |
Stencil | Yes | Yes | 100% |
Svelte | No | Yes | 94% |
Vue | No | Yes | 100% |
Compatibility score data from Custom Elements Everywhere, a test suite for web component compatibility.
Web Components are already a Success Story
Despite some notable criticism, web components are already widely used across the web.
In August 2023, Chrome Platform Status reports that 19.4% of page loads in Google Chrome were using a web component (via the CustomElementRegistryDefine
key). For comparison <img loading>
was at 15% and CSS Grid at 20%.
You can check out more social proof on: Are Web Components A Thing Yet?
They are particularly popular in large enterprise design system implementations:
Many of the component libraries listed above (even those written by authors that have criticized web components) have an export-to-web-component feature built-in.
But—what are they?
Web Components is an umbrella term that encompasses many web platform technologies and we’ll go over a couple of the big (and in my opinion, most relevant) ones.
Custom Elements
Custom Elements allow you to attach a JavaScript class
to any custom element you define on your page, to add behaviors and JavaScript-generated content to any element instances on the page. These are automatically initialized for you for the full lifecycle of the page (for server-rendered, JavaScript-generated, or content injected via fetch()
).
- Custom elements cannot be void elements (like
<img>
or<meta>
): they must have a start and end tag. - Custom elements must have a dash in the tag name, so as to not conflict with future additions to the web platform.
If you populate these with your own server-rendered nested HTML (also known as the default slot/Light DOM/plain ’ol HTML) and use Custom Elements to for behavior-only additions (and not modify rendered DOM), you can even avoid layout shift!
Some folks have started referring to this approach as creating HTML Web Components (see post for examples).
If your expectation is full stack component-driven development, you will quickly encounter a developer experience problem here when authoring components in this way. Multiple instances of the same component need to repeat the same nested content and any changes to nested content need to be applied manually to all instances. You end up writing this:
<my-counter><button>1</button></my-counter>
<my-counter><button>2</button></my-counter>
<my-counter><button>3</button></my-counter>
When we want to author this (and have it server-render as the above):
<my-counter>1</my-counter>
<my-counter>2</my-counter>
<my-counter>3</my-counter>
Shadow DOM
Shadow DOM is the next level of our web components evolution. It solves the developer experience problem with repetition in authoring markup at the expense of clientside rendering 😭.
This trade-off introduces additional complexities around managing layout shift and the flash of unstyled content (FOUC). Typical approaches to solve these problems either put all of the JavaScript into the critical rendering path (in the <head>
, which I try to avoid for performance reasons) and/or hide the components until they’re defined via JavaScript (which is by definition a performance problem for critical content).
One such approach to assert control over pre-definition and post-definition styling is the CSS pseudo-class :defined
. It’s a very useful tool for styling and progressive enhancement (occupying some of the same architectural vibes as the scripting
media query), but in Shadow DOM heavy component libraries it is often applied to hide components while loading (👎🏻 no thank you).
Declarative Shadow DOM
As we evolve to our next level of web components, we move up to Declarative Shadow DOM (sometimes known as DSD). This allows you to put your Shadow DOM template in nested markup inside of each element instance and the browser will create a shadowRoot
for you with the template contents (no JavaScript required).
This solves the clientside rendering dependency for Shadow DOM but at the expense of repetition in authoring markup! The ol’ switcheroo (in some ways) feels like a de-evolution back to the approach we discussed in Custom Elements!
Uniquely, this approach does allow you to use scoped CSS (and <slot>
) without a JavaScript dependency. Non-declarative (imperative/JavaScript) Shadow DOM offers scoped CSS too but repeats some of the mistakes made by CSS-in-JS approaches and introduces a runtime JavaScript dependency on styling.
<my-counter>
<template shadowrootmode="open">
<style>
* { color: blue; }
</style>
<button><slot></slot></button>
</template>
</my-counter>
Again, the repetition of these templates in every component instance is tedious and brittle.
We can see the tension between authoring repetition and server-side rendering, right? (Non-declarative) Shadow DOM was the only approach so far that offered a good authoring experience without repetition, but required clientside rendering.
Server-side Rendering
If we could have a reusable Declarative Shadow DOM template that did not require nesting, that would be ideal, right? (Discussion on this topic is happening in this WICG issue)
I would love something like this (though I acknowledge the likely issues with streaming here):
<!-- ⚠️ THIS CODE IS ASPIRATIONAL -->
<!-- ⚠️ IT DOESN’T WORK ANYWHERE -->
<template definition="my-counter">
<style>
button {}
</style>
<button><slot></slot></button>
</template>
<my-counter>1</my-counter>
<my-counter>2</my-counter>
<my-counter>3</my-counter>
This is why folks will claim that server-side rendering is yet to be a solved problem—it has not yet been solved at the web platform level. I’d also wager that this is the biggest remaining complaint about web components.
Folks expect this to be solved because this is a problem that many component libraries have solved. However, I would warn that we’re holding these two disparate things to different expectations.
Clientside component frameworks can and should be compared to web component specifications and tools. But if your component framework introduces an additional server rendering step or abstraction, it seems unfair to compare that to clientside-only web components.
Full stack server-rendered Svelte or React cannot and should not be compared to clientside web component specifications like Custom Elements or Shadow DOM—these are apples and oranges. The biggest thing I hear from these criticisms is that we need an additional server-rendered abstraction for web components, too.
Framework Tension
JavaScript Frameworks and Web Components can work together—though some are more web component friendly than others.
In some respects it feels like the web platform chased the early clientside rendered, single page application (SPA) architectural vibes of React, Ember, Angular, and others. The more nimble frameworks have pivoted away from those mistakes. The platform will always be a bit behind those that live further down the stack (and importantly, informed by their work) but with the right amount of patience can offer improved performance and long-term maintenance potential.
- If you want to go all-in on Web Components, have a look at these answers to the server-rendered web component question:
- React has the worst
custom-elements-everywhere.com
compatibility test score. If you’re deep in React-world, look at using Preact to provide a more future-friendly and compatible experience. - Solid, Angular, Svelte, and Vue support web components as a compilation target too.
A WebC Example
I built WebC so I guess I can now pivot this post to show you a small example of the rendered output you get when using it.
Consider a content template file index.webc
:
<my-counter>1</my-counter>
<my-counter>2</my-counter>
Classical HTML
Next, we’ll add a component definition for my-counter
to _components/my-counter.webc
:
<style webc:scoped>
:host button {}
</style>
<button><slot></slot></button>
WebC will render index.webc
as:
<style>
.wrfp4zhxg button {}
</style>
<my-counter class="wrfp4zhxg">
<button>1</button>
</my-counter>
<my-counter class="wrfp4zhxg">
<button>2</button>
</my-counter>
Declarative Shadow DOM
You can also modify your component definition (_components/my-counter.webc
) to use Declarative Shadow DOM, avoiding the issue of repeating your Shadow DOM template in your content:
<template shadowrootmode="open">
<style>
button {}
</style>
<button><slot webc:raw></slot></button>
</template>
<slot></slot>
WebC will now server-render index.webc
as:
<my-counter>
<template shadowrootmode="open">
<style>
button {}
</style>
<button><slot></slot></button>
</template>
1
</my-counter>
<my-counter>
<template shadowrootmode="open">
<style>
button {}
</style>
<button><slot></slot></button>
</template>
2
</my-counter>
This server-applied abstraction offers a variety of benefits over earlier approaches:
If you want to learn more, you can try out the eleventy-base-webc
starter project or for a deeper dive into progressive enhancement strategies have a look at Seven Progressive Enhancement Recipes using Eleventy WebC Image Comparison Components.
Appendices
This talk was given at four different events in 2023 (some of which videos are available):
- JS Heroes (May 2023)
- Smashing Conference Freiburg (September 2023)
- JS Nation (May 2023)
- Stanford Web Camp (May 2023)
19 Comments
Khalid ⚡
@zachleat The “h-i” slide cracks me up. ????
Zach Leatherman :verify:
@khalidabuhakmeh we do what we must ????
Khalid ⚡
@zachleat Great post. I’d like to see the aspirational idea you have come to fruition. The biggest issue with web component approaches, even the good ones like WebC, is that they still require a build step. This limits them to folks who are comfortable with JS build chains.I want… Truncated
Zach Leatherman :verify:
@khalidabuhakmeh 100% agree—and I’m hopeful we’ll get there!
Simon MacDonald
@khalidabuhakmeh @zachleat the aspirational ideal is literally how Enhance works. You define your template in a function and then it is expanded into light DOM custom elements by the SSR piece.We’d love to bring Enhance SSR to other languages so if you are interested in submittin… Truncated
Zach Leatherman :verify:
@macdonst @khalidabuhakmeh ah to be clear I mean that I want the aspirational idea to be a first-party platform feature.
Matt Wilcox
@khalidabuhakmeh Won't lie... it's what keeps me from investigating them any further than "the theory". I don't want node/npm on a live server. I have and trust PHP/Composer. Mostly because those are already my dependencies, and I don't want new ones with … Truncated
nrk 9819
@zachleat Great write up as always. I've been following your blog and @blog for a while for web components related articles.Also Vercel.js was funny (how we reached from Facebook.js to this point lmao ????????????)
Zach Leatherman :verify:
@mattwilcox @khalidabuhakmeh sure, but the repetition issue can be “poorly” hacked together via other mechanisms in your server-rendered stack: https://www.zachleat.com/web/webc-in-eleventy/#a-history-of-component-like-things (PHP include comes to mind)But, yeah. It’s interesting… Truncated
Zach Leatherman :verify:
@nrk9819 thank you! ????
Simon MacDonald
@zachleat @khalidabuhakmeh Same! I think we need to rally around declarative custom elements since it is too late to change the web component apis.
Khalid ⚡
@macdonst @zachleat I work with #dotnet and I don't expect MS to give folks a build-step that would allow you to process HTML before the final artifacts are built any time soon :(There is a way to possibly do it, but I think it wouldn't be ideal. dotnet
Simon MacDonald
@khalidabuhakmeh Is there no render step on the server you can tap into?
tomwor
@zachleat Vercel.js - love it. ????
Hasan Ali
@zachleat Loved the talk when I first saw it and I’m really glad there’s a written version of it ????????
Zach Leatherman :verify:
@hasanhaja it only took me 7 or 8 months to get it on the web site ???? (thank you!)
Mayank
@zachleat yea totally. declarative custom elements will need way more functionality, but we can start small
Tegan
@zachleat I guess i don’t see much of a difference between server side rendering a solid component with what webc does. They both are build steps, It’s also hard for me to see what webc gets out of rendering to a web component instead of just rendering the underlying dom.
Zach Leatherman :11ty:
@rawrmonstar I think I see what you’re saying. Fwiw, WebC components aren’t required to be Web Components—they can generate HTML without custom elements too. Moreso, WebC can generate a standalone custom element with 0 library JavaScript overhead.