Skip to content

Server Side Rendering

This lesson explains how to render Web Components on the server, use declarative Shadow DOM for first paint, hydrate custom elements on the client, and avoid common SSR pitfalls.

What Is SSR for Web Components?

Server-side rendering means generating HTML on the server before sending it to the browser. For Web Components, SSR helps improve first paint, SEO, and perceived performance, but custom elements still need client-side JavaScript to upgrade and become fully interactive.

Modern SSR for Web Components often relies on declarative Shadow DOM, fallback light DOM content, and hydration patterns that connect server HTML to client behavior.

Concept Description
SSR HTML generated on the server
Hydration Client JS attaches behavior to server HTML
Declarative Shadow DOM Shadow tree defined in server HTML
Progressive Enhancement Content works before JS fully loads
Custom Elements Require registration and upgrade on client
Main Goal Fast, accessible, indexable component output

Basic Declarative Shadow DOM SSR Example

<profile-card>
  <template shadowrootmode="open">
    <style>
      :host { display: block; }
      .card {
        padding: 1rem;
        border: 1px solid #dbe5f1;
        border-radius: 0.75rem;
      }
    </style>
    <article class="card">
      <slot></slot>
    </article>
  </template>
  <h2>Alex Chen</h2>
  <p>Frontend Developer</p>
</profile-card>

The browser can render encapsulated markup on first paint without waiting for JavaScript to call attachShadow(). Client code can still register the custom element later for interactivity.

SSR Challenges for Web Components

Challenge Why It Matters Common Solution
No JS on server attachShadow() cannot run during SSR Declarative Shadow DOM
Undefined custom elements Tags are inert until defined Fallback content and hydration
SEO and first paint Empty shells hurt UX and indexing Server-render meaningful HTML
Hydration mismatch Server and client output may differ Stable render logic and attributes
Framework differences React, Astro, and others handle SSR differently Use framework-specific integration patterns

Client Upgrade After SSR

class ProfileCard extends HTMLElement {
  connectedCallback() {
    if (!this.shadowRoot) {
      this.attachShadow({ mode: "open" });
      this.shadowRoot.innerHTML = `
        <style>
          .card { padding: 1rem; }
        </style>
        <article class="card">
          <slot></slot>
        </article>
      `;
    }

    this.classList.add("ready");
  }
}

customElements.define("profile-card", ProfileCard);

On the client, check whether a declarative shadow root already exists before calling attachShadow(). This avoids errors and supports progressive enhancement.

Progressive Enhancement Pattern

<alert-banner type="success">
  Profile saved successfully.
</alert-banner>

<noscript>
  <div class="alert alert-success" role="alert">
    Profile saved successfully.
  </div>
</noscript>

Even with SSR, some users may have JavaScript disabled or delayed. Provide meaningful HTML content inside custom elements and consider fallback markup for critical UI.

SSR with Astro

---
// profile-card.astro
const name = "Alex Chen";
const role = "Frontend Developer";
---

<profile-card>
  <template shadowrootmode="open">
    <style>
      :host { display: block; }
    </style>
    <article><slot></slot></article>
  </template>
  <h2>{name}</h2>
  <p>{role}</p>
</profile-card>

<script>
  import "./profile-card.js";
</script>

Astro is well suited for SSR because it renders HTML on the server by default. Combine server HTML with a client script that registers and hydrates the custom element.

Hydration and Event Binding

class AppButton extends HTMLElement {
  connectedCallback() {
    if (this._hydrated) return;
    this._hydrated = true;

    const button =
      this.shadowRoot?.querySelector("button") ||
      this.querySelector("button");

    button?.addEventListener("click", () => {
      this.dispatchEvent(
        new CustomEvent("app-click", {
          bubbles: true,
          composed: true
        })
      );
    });
  }
}

Hydration means attaching listeners and stateful behavior to HTML that already exists. Guard against double initialization when the component connects more than once.

SEO and Accessibility with SSR

  • Render meaningful text content inside custom elements on the server.
  • Use semantic HTML in slotted content such as headings and paragraphs.
  • Include accessible roles and labels in server HTML when possible.
  • Avoid empty custom element shells with no visible content.
  • Ensure critical content does not depend only on client rendering.
  • Test pages with JavaScript disabled for essential content visibility.

SSR vs Client-Only Rendering

Feature SSR Client-Only
First paint Faster visible content May show empty shells first
SEO Better for indexable content Depends on crawler and content
Complexity Higher setup and hydration concerns Simpler initial implementation
Shadow DOM Needs declarative shadow support Easy with attachShadow()
Best For Public pages and design system docs Internal apps and dashboards

Passing Data from Server to Component

<user-badge
  name="Alex Chen"
  role="admin"
></user-badge>
class UserBadge extends HTMLElement {
  static get observedAttributes() {
    return ["name", "role"];
  }

  connectedCallback() {
    this.render();
  }

  attributeChangedCallback() {
    this.render();
  }

  render() {
    const name = this.getAttribute("name") || "Guest";
    const role = this.getAttribute("role") || "viewer";

    if (!this.shadowRoot) {
      this.attachShadow({ mode: "open" });
    }

    this.shadowRoot.innerHTML = `
      <span class="badge">${name} (${role})</span>
    `;
  }
}

Attributes are a reliable SSR-friendly way to pass simple data from server HTML to custom elements. Use properties after hydration for richer client-side values.

When to Use SSR with Web Components

  • You need fast first paint for public marketing or docs pages.
  • SEO matters for content rendered inside custom elements.
  • You ship design system demos or documentation sites.
  • You want progressive enhancement before JS loads.
  • You use frameworks like Astro that render HTML on the server.
  • Accessibility requires visible content on initial render.

Common SSR Use Cases

  • Component library documentation sites.
  • Marketing pages using shared custom elements.
  • Blogs and content sites with encapsulated UI widgets.
  • E-commerce product cards and badges.
  • Public dashboards with initial server-rendered state.
  • Framework-agnostic UI packages distributed with SSR examples.

SSR Best Practices

  • Use declarative Shadow DOM when encapsulated SSR markup is needed.
  • Render meaningful fallback content inside custom element tags.
  • Pass simple server data through attributes.
  • Guard hydration logic against double initialization.
  • Keep server and client render output consistent.
  • Load component definition scripts as modules on the client.
  • Test with slow networks and JavaScript disabled.

Common SSR Mistakes

  • Rendering empty custom elements with no server content.
  • Calling attachShadow() when a declarative root already exists.
  • Assuming SSR alone makes components interactive.
  • Relying on properties instead of attributes for initial server data.
  • Creating hydration mismatches between server and client HTML.
  • Hiding all content inside JS-only shadow rendering.
  • Ignoring browser support for declarative Shadow DOM.

Key Takeaways

  • Web Components can be server-rendered, but still need client upgrade.
  • Declarative Shadow DOM is key for encapsulated SSR markup.
  • Attributes and slotted content make SSR data passing practical.
  • Hydration attaches interactivity to existing server HTML.
  • Good SSR improves first paint, SEO, and progressive enhancement.

Pro Tip

Ship server HTML that is useful on its own, then enhance it on the client. If the page is readable before JavaScript loads, your SSR strategy for Web Components is on the right track.