Learning about Web Components: Part 4 – Researching pre-rendering custom elements and/or SSR
⭐️ a blog post
Now that I’m starting to get a handle on building custom elements which can do things dynamically in the browser, I also want to see how difficult it would be to implement “server side rendering” for my custom elements. There is a noticeable delay when first loading my photo grid demo – the browser must download, parse, and run my javascript, instantiate my custom elements, they then do a simulated database query, and then finally the image grid can render.
You can see the delay in this screen recording:
I’d prefer if the HTML returned from the server could already include the first version of the image grid, then the javascript could take over from there once it’s ready in the browser.
The announcement of Declarative Shadow DOM in WebKit and an update about support in Chromium are what really got me started looking into web components again in the first place. The promise of being able to serve HTML which can be shown without any javascript means web components are finally ready for use 💪 Firefox has announced they also will be implementing the agreed upon declarative API, so the future is finally here.
The first project I found which talked about declarative shadow DOM + SSR was ocean – which provides “web component
server-side rendering” in deno or a service worker. lit-labs/ssr
and
astro’s SSR are
also very cool libraries.
But those never really clicked for me. None of them seem to be both fully up-to-date with the latest specification + able to render any custom element I might build.
It clicked for me today when I was rereading the two announcements and I finally noticed something I hadn’t noticed before.
From the Chromium page:
Because the contents of a serialized Declarative Shadow Root are entirely static…
From the WebKit page:
In the following example, the outer template element contains an instance of some-component element and its shadow tree content is serialized using declarative shadow DOM.
Both pages mention that declarative shadow DOM is supposed to be the serialized version
of a living shadow root. This made more sense when I found an older page talking about
a not-yet-standardized and chromium-only built-in function to serialize a custom
element getInnerHTML()
. The example shows:
const html = element.getInnerHTML({ includeShadowRoots: true });
I am more interested in more of an outerHTML
equivalent, but still I
can work with this.
Calling getInnerHTML()
on the <image-grid>
in the
demo page from part 3 of this series in
Chrome returns:
<image-cell record-id="5">
<!-- this shadowroot attribute is old and deprecated, see below -->
<template shadowroot="open">
<style>
:host {
align-items: center;
display: flex;
flex-direction: column;
height: 100%;
justify-content: center;
width: 100%;
}
img {
max-width: 100%;
width: auto;
height: auto;
object-fit: contain;
overflow: hidden;
}
.name {
display: block;
min-height: min-content;
text-align: center;
}
</style>
<img alt="Photo of a lot of differently colored tulips" src="./five.jpg" width="1000" height="662">
<p class="name">five.jpg</p>
</template>
</image-cell>
<image-cell record-id="4">
<!-- this shadowroot attribute is old and deprecated, see below -->
<template shadowroot="open">
<style>
:host {
align-items: center;
/* ... */
}
/* ... */
</style>
<img alt="Photo through a hexagonal window into a tea room on a sunny day" src="./four.jpg" width="662" height="1000">
<p class="name">four.jpg</p>
</template>
</image-cell>
<!-- ... -->
<template shadowrootmode="open">
<style>
:host {
box-sizing: border-box;
display: grid;
gap: 16px;
grid-template-columns: 1fr 1fr;
margin: 0 auto;
max-width: 600px;
padding: 20px;
}
</style>
<slot></slot>
</template>
The first thing to unpack here is: Chrome’s non-yet-standardized
getInnerHTML()
function is returning <template>
elements with the old and deprecated shadowroot
attribute. So this
function isn’t going to work out-of-the-box for me.
Still, it never occurred to me that the DOM might know how to serialize itself. That SSR needn’t be so much about rendering templates or munging strings – instead be about building up a DOM to a state, then serializing it in a fully presentable way. This feels simpler and easier for me to think about now.
The second thing to notice is: when we serialize a shadow root, we
must include a full template every time. That means if we have scoped
<style>
s in the <template>
, they will be repeated
over and over for every instance of the custom element. We could handle this by
extracting the CSS to a file and @import
ing it if we cared about
optimizing things.
The Chrome page says we don’t have to worry:
Styles specified this way are also highly optimized: if the same stylesheet is present in multiple Declarative Shadow Roots, it is only loaded and parsed once. The browser uses a single backing
CSSStyleSheet
that is shared by all of the shadow roots, eliminating duplicate memory overhead.
The third thing to notice is: our custom element will have a
shadowRoot
before our code calls attachShadow()
. We don’t
technically have to worry about this: calling attachShadow()
will clear
and return the existing shadowRoot
which was created by the HTML parser
when it found the <template>
tag in our HTML.
From this information, I figured I could quickly build my own serializer which outputs
the up-to-date <template shadowrootmode=open>
attribute and works
more like outerHTML
.
I found a “getInnerHTML()
polyfill” written by @developit
(who works on preact and I follow on mastodon). It’s a bit terse and
took a bit for me to grasp, but it’s nice to see that it can be a really short and
simple function.
Here is my version. It’s a very quick draft which works for me in all the current major browsers. It won’t work for every use case and isn’t ready for production… it is a proof-of-concept though.
Here is the output from my serializer of the <image-grid>
custom
element from the previous demo page:
<image-grid aria-live="polite" role="region" aria-label="Image grid">
<image-cell record-id="1">
<template shadowrootmode="open">
<style>
/* ... */
</style>
<img alt="Photo of a building on a sunny day" src="./one.jpg" width="1000" height="838">
<p class="name">one.jpg</p>
</template>
</image-cell>
<image-cell record-id="4">
<!-- ... -->
</image-cell>
<!-- ... -->
<template shadowrootmode="open">
<style>
/* ... */
</style>
<slot></slot>
</template>
</image-grid>
OK! I copied and pasted the serialized html from the browser’s console to a new
demo-serialized.html
file, I did some quick hacking to make the components
check if the shadowRoot
already exists, and now I have an example of what
the resulting HTML could be like if I could do the serialization on the server.
I recorded a screencast of the page loading now, which is basically immediate:
The javascript kicks in and the images continue to re-sort every 10 seconds, just like before.
What’s next?
I don’t yet know how to run the serializer on the server – maybe one of those “mock DOM libraries” could make it all work out. When I get some free time I’ll dive into that.
If you have any feedback about any of this, if I made any mistakes, or if you just want to chat about web components or anything else, then find me on mastodon 👋