avatarDanny Moerkerke

Summary

The provided content discusses the method of server-side rendering for Web Components using Declarative Shadow DOM to improve performance and SEO.

Abstract

The article explains the concept of server-side rendering (SSR) for Web Components, emphasizing the benefits of using Declarative Shadow DOM for faster page loads and better search engine optimization. It details the issues with client-side rendering, particularly for Single Page Applications (SPAs), where dynamic content is fetched after the initial skeleton HTML is loaded, leading to slower first-page loads and SEO challenges. The introduction of Declarative Shadow DOM allows developers to include the Shadow DOM directly in the HTML document, which is then rendered on the server before reaching the client. This approach ensures that the styled content is immediately visible to users and search engines. The article also addresses potential issues in non-supporting browsers and provides solutions using CSS pseudo-classes to hide unstyled content. It concludes by introducing the getInnerHTML() method for serialization, which allows for the conversion of Custom Elements to include the Shadow Root for SSR without losing the benefits of reusable components. The article ends by teasing the concept of hydration for making server-side rendered components interactive and invites readers to subscribe to a newsletter for more insights on modern web development.

Opinions

  • The author suggests that server-side rendering is superior to client-side rendering for initial page load performance and SEO.
  • Declarative Shadow DOM is presented as a significant advancement for Web Components, enabling true server-side rendering of components.
  • The author implies that the use of CSS pseudo-classes to hide content in non-supporting browsers is a necessary workaround until broader support for Declarative Shadow DOM is achieved.
  • The article conveys that the getInnerHTML() method is a crucial tool for developers to serialize Custom Elements with Shadow DOM for SSR purposes.
  • The author believes that hydration is an essential step after SSR to ensure that Web Components are interactive and fully functional on the client side.
  • By inviting readers to join a newsletter, the author expresses a commitment to ongoing education and discourse in the field of modern web development, particularly around Web Components and Progressive Web Apps.

How To Server-Side Render A Web Component

Blazing fast lazy-loaded Web Components

Photo by Xavi Cabrera on Unsplash

Why server-sider rendering?

Server-side rendering means the server sends a complete HTML document to the browser which then renders it. This is in contrast to client-side rendering where a partial HTML page is sent to the browser and then JavaScript renders the rest in the browser.

When a browser requests an HTML page, it’s often changed by JavaScript before it’s shown to the user. This means that the HTML that is sent by the server is not always the same as what the user sees. This is especially true for Single Page Apps (SPA) and JavaScript frameworks that enable developers to write components.

In the case of a SPA, only the so-called “skeleton” HTML is sent to the browser. This skeleton is the layout that is used on all pages and usually consists of only the header and footer. For each page, dynamic content is fetched which is then placed between the header and footer.

When a user then navigates to another page, only the dynamic content is fetched and also placed between the header and footer, replacing any content that was already present. While this enables very fast page navigations because only the dynamic content is replaced, it does mean that the very first page load is slower because after the skeleton is loaded, the dynamic content needs to be fetched separately.

This is also bad for Search Engine Optimization (SEO) because search engine crawlers usually don’t execute JavaScript so they will only “see” the skeleton and not the dynamic content which will now not be indexed.

Server-side rendering can solve both problems by rendering a full page for the first visit of a user (header + dynamic content + footer). This first page will now be loaded faster and any subsequent navigations will be handled by replacing the dynamic content only. Search engine crawlers will always be served a full page so all content can be indexed.

In the case of JavaScript frameworks that enable the creation of components, these components are always rendered in the browser using JavaScript. The page HTML usually only contains a single HTML tag for the component which will be rendered to the component’s full HTML in the browser. By doing this rendering already on the server, the component’s HTML will already be part of the webpage so it will be immediately visible, even before its JavaScript code has been loaded.

For Custom Elements, this should work in the same way, but this doesn’t work when using Shadow DOM because there’s no way to represent a Shadow Root in plain HTML: you cannot write Shadow DOM directly into an HTML document. To make Shadow DOM part of the webpage it would need to be added declaratively: you write (declare) it as plain HTML which is part of the webpage.

But unfortunately, the only way to create a Shadow Root is imperatively: you write JavaScript code that instructs the browser to create it.

Until now.

Declarative Shadow DOM

Declarative Shadow DOM enables developers to write the HTML for the Shadow DOM directly in the document. It is added to a Custom Element using a <template> element with a shadowrootmode attribute.

The `shadowrootmode` attribute used to be `shadowroot`. Some older browsers still only support the latter.

When the HTML parser encounters such a <template> it is immediately converted and added as a Shadow Root to its parent element:

<html>
  <head></head>
  <body>

    <my-element>
      <template shadowrootmode="open">
        <style>
          h1 {
            color: red;
          }

          ::slotted(p) {
            color: green;
          }
        </style>

        <h1>Declarative Shadow DOM</h1>
        <slot></slot>
      </template>

      <p>This is a light DOM</p>
    </my-element>

  </body>
</html>

In supporting browsers, you will see red heading text and a paragraph in green text:

The beauty of this approach is that the Shadow root will be attached to the Custom Element and rendered with its internal CSS applied without any JavaScript.

Note that the <template> element is replaced by its content.

In non-supporting browsers, you will only see the light DOM which in this case is the <p> tag containing the text “This is light DOM”. This is a potential issue in non-supporting browsers because it can mean that unstyled content will be shown.

This can be avoided by combining the :not() and :defined CSS pseudo-classes to hide my-element while it hasn’t been upgraded to a Custom Element yet:

my-element:not(:defined) {
  display: none;
}

This ensures my-element is not visible until it is defined, but since this will also apply in browsers that do support Declarative Shadow DOM, it won’t be visible there either. Fortunately, you can take advantage of the fact that supporting browsers replace the <template> with its content, while non-supporting browsers will not.

The CSS rule can now be expanded to target the Custom Elements that do have a <template> with a shadowrootmode attribute and hide the content that comes after it (the light DOM):

my-element:not(:defined) > template[shadowrootmode] ~ *  {
  display: none;
}

This rule targets the <template shadowrootmode="..."> inside <my-element> and uses the general sibling selector ~to target any content (*) that comes after it.

The following example will show the declarative Shadow DOM of the element in supporting browsers and nothing in non-supporting browsers:

Serialization

At this point, you might wonder if you would have to convert all your component to use declarative Shadow DOM if you need server-side rendering. You probably also think to yourself that this defeats the purpose of Shadow DOM since now you need to put the Shadow DOM in the tag of your component itself and all encapsulation is lost.

What’s the use of reusable Custom Elements when every time you want to reuse it, you need to write the whole Shadow Root instead of just the HTML tag?

Don’t worry, you won’t have to.

You will use your Custom Elements by just using their tags. Whenever you need server-side rendering, you will use a build task that will convert these to components that contain a declarative Shadow Root. For this purpose, the getInnerHTML() method has been added to HTMLElement.

This method works just like the innerHTML property but provides an option to specify if the Shadow Root should be returned as well:

element.getInnerHTML({includeShadowRoots: false}); // will not return the Shadow Root

element.getInnerHTML({includeShadowRoots: true});  // will include the Shadow Root

element.getInnerHTML();  // will include the Shadow Root

Note that includeShadowRoots: true is the default so without this option the Shadow Root will be returned. You need to explicitly specify includeShadowRoots: false to exclude it from the result. When the Shadow Root is excluded any light DOM will still be returned.

A build task can now be executed on the server that selects all Custom Elements on the page, calls getInnerHTML() on each element, and then replaces its content (innerHTML) with the returned value.

When getInnerHTML() is called on a Custom Element that has an imperative Shadow Root (added with JavaScript) it will be returned conveniently wrapped in a <template shadowroot="open"> element:

<my-element>
  <p>This is an imperative Shadow Root</p>
</my-element>
class MyElement extends HTMLElement {
  constructor() {
    super();

    const shadowRoot = this.attachShadow({mode: 'open'});

    shadowRoot.innerHTML = `
      <h1>Imperative Shadow DOM</h1>
      <slot></slot>
    `;
  }
}

MyElement.getInnerHTML();

will return:

<p>This is an imperative Shadow Root</p>
<template shadowrootmode="open">
  <h1>Imperative Shadow DOM</h1>
  <slot></slot>
</template>

even though this element does NOT have a declarative Shadow Root!

Conclusion

Declarative Shadow DOM enables us to attach a Shadow root to a Custom Element and fully render it without any JavaScript. This is a huge step for web components since they can now be rendered on the server. But there is a slight problem though.

Our component doesn’t do anything, it’s not interactive.

If you check the server-side rendered components from the previous examples in the browser’s dev tools you will notice they all have a Shadow root attached to it. But when you check the CustomElementsRegistry to find the constructor of the element:

const el = await customElements.get('my-element');

console.log(el) // undefined 😱

you will notice that it hasn’t even been registered as a Custom Element.

This is an important fact to realise about Declarative Shadow DOM: it only attaches a Shadow root to an element.

In other words, it only takes care of rendering the HTML of the component and nothing else. The benefit of this approach is that it enables web components to be rendered very fast, but after that, they still need to be registered as Custom Elements and be made interactive.

That is what hydration does. In my next article I explain you how to hydrate a server-side rendered Web Component.

Join Modern Web Weekly, my weekly newsletter on the modern web platform, web components and Progressive Web Apps.

JavaScript
Web Development
Frontend
Web Components
Software Development
Recommended from ReadMedium