Web components seem to be picking up a lot of steam lately and there have been some concerns about their compatibility with client-side frameworks pre-rendering or server-side rendering client-side content on Node.js servers.
There are ways to get your components working with SRRing, but is it worth actually SSRing your web components, or is client-side rendering enough? When exploring this question, the prevalent answer is the classic "it depends".
It Depends
Two things to consider when deciding if and what you should be server-side rendering for web components.
The first is, whether or not your web components have slow operations like data fetching. In this case, it may be a good idea to load some content from the server for the initial load.
The second comes down to user experience and your user expectations. The benefits usually associated with SSRing your content are reduced dependency on JavaScript, improved SEO, and enhanced performance.
There are many ways to use web components in your applications, but for this exploration, I will be focusing the discussion on "leaf node" primitives or small "dumb" components that receive data, and render some UI like those that would be found in a design system.
Reduced JavaScript
Web components can help reduce JavaScript dependency by removing dependencies on libraries and frameworks. If your framework is executing on the server, web components can be leveraged to handle the client-side user interactivity.
An important note is that defining custom elements is part of the JavaScript DOM API, which means there is a JavaScript dependency. That may be a concern for some teams, but the good news is there are current and forthcoming ways to create them without JavaScript.
The nice thing is that the JavaScript that is used to execute custom elements is extremely efficient which can make them wicked fast! We'll discuss more about that in the performance section of this article.
Improved SEO
When content is server-side rendered, content can be pre-fetched and JavaScript can be pre-rendered which means web crawlers and SEO bots don't have to wait for your code to be loaded before indexing your content. They often won't wait for your asynchronous data to return before analyzing the page.
Fortunately, client-side rendered web components work fine for SEO, including content rendered in the shadow DOM. You can read more about it in an article I wrote last year.
Improved Performance
One of the nice things about web components is that they are parsed and rendered directly by the browser rather than having a library parse and render the UI (again, wicked fast.
Also, isolating content in the shadow DOM allows CSS and JavaScript selectors to perform more efficiently both in and out of your custom elements. Using selectors within your component allows them to stay scoped within the shadow DOM rather than querying the entire document for elements that match the parameters. Likewise, global styles and scripts skip over shadow DOM content which reduces the number of elements they need to query.
I created a simple example with a bunch of Shoelace components where they are being lazy-loaded from a CDN. I loaded the components this way to show worst-case-scenario loading performance. As you can see, it still loads quite quickly.
After many, many attempts, the worst score I could get from Lighthouse on performance was a 94 (part of this may have been from too many people watching Netflix at my house affecting CDN download times).
If these were being eager-loaded from the same server, the performance would be consistently better!
But What About FOUC???
I believe FOUC (flash of unstyled content) is a legitimate concern for web components and should be something teams should take into consideration, especially for things like layout components. While the component loads and applies the styles and JavaScript content within and around them can shift which can negatively affect the user experience.
As you can see from the example above, the reason for the lower score was due to the Cumulative Layout Shift (CLS) that occurs as the components load and render. Again, the components are being lazy-loaded from a CDN, so this will be slightly exaggerated from some typical implementations, which helps emphasize the point.
An approach I have commonly used to reduce this is to use CSS to hide elements that have yet to be defined.
:not(:defined) {
visibility: hidden;
}
This works better than display: none;
because it allows the component to continue to occupy space while it loads and shifts around, which can reduce layout shifting, but it's not great. Then I came across this article by Cory LaViska that goes into greater detail about working with custom elements and FOUC as well as providing alternative solutions. Based on this and after discussing it with others, I have settled on this solution for now.
<style>
body:not(.wc-loaded) {
opacity: 0;
}
</style>
<script type="module">
(() => {
Promise.allSettled(
[...document.querySelectorAll(":not(:defined)")].map((component) =>
customElements.whenDefined(component.tagName.toLowerCase())
)
).then(() => document.body.classList.add("wc-loaded"));
})();
</script>
This is a very simple solution that can be tweaked and modified to meet your environment's needs. You can see how it smooths out the load experience in this example. Also, it's improved our Lighthouse score!
Conclusion
So after all this, the million-dollar question is do teams need to SSR their web components? The answer really is "it depends" and this information should be evaluated based on your application and your user's needs.
But, for most teams using web components for their design systems and other "leaf nodes" in their projects, it's probably not worth it. They already run very lean JavaScript, are tuned for performance, and work fine for SEO, so the benefits gained by trying to SSR your components may be trivial in comparison to the work it takes to get them working for each framework.
Top comments (4)
As someone exploring the possibilities of WebComponents, this article was very informative and I found it fascinating! Thank you.
By the way, regarding the final solution, I thought that the same thing could be achieved with just CSS too.
Also note
Is going to be interesting in an environment where additional Web Components are lazy loaded
It is still waiting for the slowest Web Component load.
Kinda like waiting for the last car to finish in Las Vegas to say "Max wins the F1"
True, but these are based on component definition and not component load. If you have a component that loads slowly, you can update the script to base on load and exclude that component from the list.