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.