{"id":6465,"date":"2025-06-25T00:54:25","date_gmt":"2025-06-25T04:54:25","guid":{"rendered":"https:\/\/ionic.io\/blog\/?p=6465"},"modified":"2025-06-25T01:05:59","modified_gmt":"2025-06-25T05:05:59","slug":"the-quest-for-ssr-with-web-components-a-stencil-developers-journey","status":"publish","type":"post","link":"https:\/\/ionic.io\/blog\/the-quest-for-ssr-with-web-components-a-stencil-developers-journey","title":{"rendered":"The Quest for SSR with Web Components: A Stencil Developer&#8217;s Journey"},"content":{"rendered":"\n<p>Picture this: You&#8217;ve built a beautiful design system using Web Components. Your components are framework-agnostic, encapsulated, and performant. Life is good&#8230; until someone asks, &#8220;But does it work with SSR?&#8221;<\/p>\n\n\n\n<p><em>Record scratch. Freeze frame.<\/em><\/p>\n\n\n\n<p>That&#8217;s where our story begins.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\" id=\"h-the-promise-land-ssr-and-web-components\"><strong>The Promise Land: SSR and Web Components<\/strong><\/h2>\n\n\n\n<p>Before we dive into the trenches, let&#8217;s set the stage. Server-Side Rendering (SSR) is the golden child of modern web development. It promises:<\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li><strong>Lightning-fast initial loads<\/strong> \u2014 Users see content immediately, no more staring at blank screens<\/li>\n\n\n\n<li><strong>SEO superpowers<\/strong> \u2014 Search engines love fully-formed HTML<\/li>\n\n\n\n<li><strong>Enhanced security<\/strong> \u2014 Keep your secrets on the server where they belong<\/li>\n<\/ul>\n\n\n\n<p>Meanwhile, Web Components offer their own slice of paradise:<\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li><strong>Framework agnosticism<\/strong> \u2014 Works with React, Vue, Angular, or vanilla JS<\/li>\n\n\n\n<li><strong>True encapsulation<\/strong> \u2014 Shadow DOM keeps your styles from leaking everywhere<\/li>\n\n\n\n<li><strong>Native performance<\/strong> \u2014 No framework overhead, just pure browser APIs<\/li>\n<\/ul>\n\n\n\n<p>So naturally, combining these two should create the ultimate web development utopia, right?<\/p>\n\n\n\n<p><em>Narrator: It did not.<\/em><\/p>\n\n\n\n<h2 class=\"wp-block-heading\" id=\"h-when-worlds-collide-the-challenges\"><strong>When Worlds Collide: The Challenges<\/strong><\/h2>\n\n\n\n<p>Here&#8217;s the thing about Web Components \u2014 they&#8217;re inherently client-side creatures. They rely on browser APIs like customElements.define() and the Shadow DOM, which don&#8217;t exist in Node.js land. It&#8217;s like trying to teach a fish to climb a tree.<\/p>\n\n\n\n<p>The main challenges we faced:<\/p>\n\n\n\n<h3 class=\"wp-block-heading\" id=\"h-1-the-browser-dependency-problem\"><strong>1. The Browser Dependency Problem<\/strong><\/h3>\n\n\n\n<p>Web Components love their browser APIs (window, document, localStorage). Servers? Not so much. Running these components on the server is like asking your coffee machine to make tea \u2014 technically possible, but requires some creative engineering.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\" id=\"h-2-shadow-dom-shenanigans\"><strong>2. Shadow DOM Shenanigans<\/strong><\/h3>\n\n\n\n<p>The Shadow DOM is fantastic for encapsulation, but servers don&#8217;t speak Shadow DOM. We needed to somehow replicate this encapsulation server-side, which led us down the rabbit hole of Declarative Shadow DOM.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\" id=\"h-3-the-hydration-headache\"><strong>3. The Hydration Headache<\/strong><\/h3>\n\n\n\n<p>Hydration \u2014 the process where client-side JavaScript takes over server-rendered HTML \u2014 becomes particularly tricky with Web Components. It&#8217;s like performing a magic trick where you swap a cardboard cutout with a real person without anyone noticing.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\" id=\"h-enter-the-arena-different-approaches\"><strong>Enter the Arena: Different Approaches<\/strong><\/h2>\n\n\n\n<h3 class=\"wp-block-heading\" id=\"h-lit-s-take-keep-it-simple\"><strong>Lit&#8217;s Take: &#8220;Keep It Simple&#8221;<\/strong><\/h3>\n\n\n\n<p>Lit approached SSR with elegance. Their solution renders components to static HTML using a low-level library that doesn&#8217;t fully emulate the DOM. Here&#8217;s what a Lit component looks like when server-rendered:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>&lt;simple-greeter name=&quot;Friend&quot;&gt;\n\n\u00a0\u00a0&lt;template shadowroot=&quot;open&quot; shadowrootmode=&quot;open&quot;&gt;\n\n\u00a0\u00a0\u00a0\u00a0&lt;style&gt;\/* component styles *\/&lt;\/style&gt;\n\n\u00a0\u00a0\u00a0\u00a0&lt;div&gt;\n\n\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0&lt;h1&gt;Hello, &lt;span&gt;Friend&lt;\/span&gt;!&lt;\/h1&gt;\n\n\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0&lt;p&gt;Count: 0&lt;\/p&gt;\n\n\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0&lt;button&gt;++&lt;\/button&gt;\n\n\u00a0\u00a0\u00a0\u00a0&lt;\/div&gt;\n\n\u00a0\u00a0&lt;\/template&gt;\n\n&lt;\/simple-greeter&gt;<\/code><\/pre>\n\n\n\n<p>Clean, declarative, and it works. But here&#8217;s the catch \u2014 there aren&#8217;t many real-world examples of this being used in large-scale design systems. It&#8217;s like having a sports car that looks amazing but you&#8217;re not quite sure how it handles on actual roads.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\" id=\"h-stencil-s-journey-the-kitchen-sink-approach\"><strong>Stencil&#8217;s Journey: The Kitchen Sink Approach<\/strong><\/h3>\n\n\n\n<p>At Stencil, we decided to throw everything at the problem. Our toolkit includes:<\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li>A <strong>hydrate module<\/strong> that creates a virtual DOM environment (MockDoc) on the server<\/li>\n\n\n\n<li><strong>renderToString<\/strong> and <strong>streamToString<\/strong> functions for serialization<\/li>\n\n\n\n<li><strong>serializeProperty<\/strong> to handle complex objects, Maps, Sets, and even Infinity (because why not?)<\/li>\n<\/ul>\n\n\n\n<p>Here&#8217;s the twist: we ended up with TWO different approaches because one size definitely doesn&#8217;t fit all.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\" id=\"h-the-tale-of-two-strategies\"><strong>The Tale of Two Strategies<\/strong><\/h2>\n\n\n\n<h3 class=\"wp-block-heading\" id=\"h-strategy-1-the-compiler-approach-when-you-need-universal-ssr\"><strong>Strategy 1: The Compiler Approach (When You Need Universal SSR)<\/strong><\/h3>\n\n\n\n<p>Our compiler-based approach, implemented in the @stencil\/ssr package, works like a skilled translator at a United Nations meeting. It operates as a Vite or Webpack plugin, intercepting your code at build time and performing some serious AST (Abstract Syntax Tree) surgery.<\/p>\n\n\n\n<p>Here&#8217;s the magic: when you write this innocent-looking React component:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>import { MyComponent } from &#039;component-library-react&#039;\n\nfunction App() {\n\n\u00a0\u00a0return (\n\n\u00a0\u00a0\u00a0\u00a0&lt;div&gt;\n\n\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0&lt;h1&gt;SSR Test&lt;\/h1&gt;\n\n\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0&lt;MyComponent first=&quot;Stencil&quot; middleName=&quot;&#039;Don&#039;t call me a framework&#039;&quot; last=&quot;JS&quot; \/&gt;\n\n\u00a0\u00a0\u00a0\u00a0&lt;\/div&gt;\n\n\u00a0\u00a0)\n\n}<\/code><\/pre>\n\n\n\n<p>The plugin receives it as a pile of jsx calls after transformation:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>import { jsxDEV } from &quot;react\/jsx-dev-runtime&quot;;\n\n\/* @__PURE__ *\/ jsxDEV(MyComponent, {\u00a0\n\n\u00a0\u00a0first: &quot;Stencil&quot;,\u00a0\n\n\u00a0\u00a0middleName: &quot;&#039;Don&#039;t call me a framework&#039;&quot;,\u00a0\n\n\u00a0\u00a0last: &quot;JS&quot;\u00a0\n\n}, void 0, false, ...this)<\/code><\/pre>\n\n\n\n<p>Now comes the clever part. The plugin:<\/p>\n\n\n\n<ol class=\"wp-block-list\">\n<li>Parses this JavaScript into an AST<\/li>\n\n\n\n<li>Identifies which components need SSR<\/li>\n\n\n\n<li>Analyzes the props being passed<\/li>\n\n\n\n<li>Calls our hydrate module to pre-render the component<\/li>\n\n\n\n<li>Replaces the original component with a wrapper containing the pre-rendered HTML<\/li>\n<\/ol>\n\n\n\n<p>The result? Your MyComponent becomes MyComponent$0:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>const MyComponent$0 = ({ children, ...props }) =&gt; {\n\n\u00a0\u00a0return &lt;my-component class=&quot;hydrated sc-my-component-h&quot;\u00a0\n\n\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0first=&quot;Stencil&quot;\u00a0\n\n\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0last=&quot;JS&quot;\u00a0\n\n\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0middle-name=&quot;&#039;Don&#039;t call me a framework&#039;&quot;\u00a0\n\n\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0s-id=&quot;1&quot;\u00a0\n\n\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0suppressHydrationWarning={true}\u00a0\n\n\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0{...props}&gt;\n\n\u00a0\u00a0\u00a0\u00a0&lt;template shadowrootmode=&quot;open&quot; suppressHydrationWarning={true}\u00a0\n\n\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0dangerouslySetInnerHTML={{\u00a0\n\n\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0__html: `&lt;style&gt;:host{display:block;color:green}&lt;\/style&gt;\n\n\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0&lt;div c-id=&quot;1.0.0.0&quot; class=&quot;sc-my-component&quot;&gt;\n\n\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0&lt;!--t.1.1.1.0--&gt;\n\n\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0Hello, World! I&#039;m Stencil &#039;Don&#039;t call me a framework&#039; JS\n\n\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0&lt;\/div&gt;`\u00a0\n\n\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0}}&gt;&lt;\/template&gt;\n\n\u00a0\u00a0\u00a0\u00a0{children}\n\n\u00a0\u00a0&lt;\/my-component&gt;\n\n}<\/code><\/pre>\n\n\n\n<p>For Next.js specifically, we go even fancier with dynamic imports:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>const MyComponent$0Instance = dynamic(\n\n\u00a0\u00a0() =&gt; componentImport.then(mod =&gt; mod.MyComponent),\n\n\u00a0\u00a0{\n\n\u00a0\u00a0\u00a0\u00a0ssr: false,\n\n\u00a0\u00a0\u00a0\u00a0loading: () =&gt; &lt;MyComponent$0 {...props}&gt;{children}&lt;\/MyComponent$0&gt;\n\n\u00a0\u00a0}\n\n)<\/code><\/pre>\n\n\n\n<p><strong>The Pros:<\/strong><\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li>Works with any React meta-framework (Vite, Remix, Next.js)<\/li>\n\n\n\n<li>Handles deeply nested component compositions beautifully<\/li>\n\n\n\n<li>No runtime overhead on the server<\/li>\n\n\n\n<li>Clean separation of concerns<\/li>\n<\/ul>\n\n\n\n<p><strong>The Cons:<\/strong><\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li>Can&#8217;t resolve dynamic props at compile time (e.g., prop={calculateValue()})<\/li>\n\n\n\n<li>No access to Light DOM during serialization<\/li>\n\n\n\n<li>Still prone to occasional hydration mismatches<\/li>\n\n\n\n<li>Requires build-time configuration<\/li>\n<\/ul>\n\n\n\n<h3 class=\"wp-block-heading\" id=\"h-strategy-2-the-runtime-approach-when-you-re-all-in-on-next-js\"><strong>Strategy 2: The Runtime Approach (When You&#8217;re All-In on Next.js)<\/strong><\/h3>\n\n\n\n<p>The runtime approach is like having a personal chef instead of meal prep. When Next.js hits a Stencil component during server rendering, we spring into action in real-time.<\/p>\n\n\n\n<p>This approach leverages Next.js Server Components, which support async operations. Here&#8217;s how it works:<\/p>\n\n\n\n<ol class=\"wp-block-list\">\n<li><strong>Component Analysis<\/strong>: When the server encounters a Stencil component, we intercept it<\/li>\n\n\n\n<li><strong>Prop Serialization<\/strong>: We use serializeProperty to handle all props, including complex objects<\/li>\n\n\n\n<li><strong>Children Transformation<\/strong>: Here&#8217;s where it gets interesting \u2014 we attempt to transform the React children into a string using react-dom\/server<\/li>\n\n\n\n<li><strong>Async Rendering<\/strong>: We call Stencil&#8217;s renderToString (which returns a Promise) right there on the server<\/li>\n\n\n\n<li><strong>React Node Recreation<\/strong>: We parse the resulting HTML back into React nodes using html-react-parser<\/li>\n<\/ol>\n\n\n\n<p>The implementation looks something like this:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>\/\/ Server Component Wrapper\n\nasync function MyComponentSSR({ children, ...props }) {\n\n\u00a0\u00a0\/\/ Serialize all props for Stencil\n\n\u00a0\u00a0const serializedProps = Object.entries(props)\n\n\u00a0\u00a0\u00a0\u00a0.map((&#091;key, value]) =&gt; `${key}=&quot;${serializeProperty(value)}&quot;`)\n\n\u00a0\u00a0\u00a0\u00a0.join(&#039; &#039;);\n\n\u00a0\u00a0\/\/ Attempt to render children to string\n\n\u00a0\u00a0let childrenHtml = &#039;&#039;;\n\n\u00a0\u00a0try {\n\n\u00a0\u00a0\u00a0\u00a0childrenHtml = ReactDOMServer.renderToString(children);\n\n\u00a0\u00a0} catch (e) {\n\n\u00a0\u00a0\u00a0\u00a0\/\/ Handle nested server components gracefully\n\n\u00a0\u00a0\u00a0\u00a0console.warn(&#039;Complex children detected, using fallback&#039;);\n\n\u00a0\u00a0}\n\n\u00a0\u00a0\/\/ Render the component with Stencil&#039;s SSR\n\n\u00a0\u00a0const { html } = await renderToString(\n\n\u00a0\u00a0\u00a0\u00a0`&lt;my-component ${serializedProps}&gt;${childrenHtml}&lt;\/my-component&gt;`,\n\n\u00a0\u00a0\u00a0\u00a0{ prettyHtml: true }\n\n\u00a0\u00a0);\n\n\u00a0\u00a0\/\/ Parse back to React\n\n\u00a0\u00a0return parseHtmlToReact(html, {\u00a0\n\n\u00a0\u00a0\u00a0\u00a0suppressHydrationWarning: true\u00a0\n\n\u00a0\u00a0});\n\n}<\/code><\/pre>\n\n\n\n<p><strong>The Pros:<\/strong><\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li>Full access to all props at runtime<\/li>\n\n\n\n<li>Can include Light DOM in serialization<\/li>\n\n\n\n<li>Handles dynamic values perfectly<\/li>\n\n\n\n<li>Enables true isomorphic rendering<\/li>\n<\/ul>\n\n\n\n<p><strong>The Cons:<\/strong><\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li>Next.js only (Server Components required)<\/li>\n\n\n\n<li>Requires managing dual component wrappers (client + server)<\/li>\n\n\n\n<li>Performance overhead from runtime serialization<\/li>\n\n\n\n<li>Complex children (multiple server components) can fail<\/li>\n\n\n\n<li>Higher hydration mismatch risk<\/li>\n<\/ul>\n\n\n\n<h3 class=\"wp-block-heading\" id=\"h-choosing-your-fighter\"><strong>Choosing Your Fighter<\/strong><\/h3>\n\n\n\n<p>So which approach should you use? Here&#8217;s our battle-tested decision tree:<\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li><strong>Use the Compiler Approach when:<\/strong><strong><br><\/strong>\n<ul class=\"wp-block-list\">\n<li>You need to support multiple frameworks<\/li>\n\n\n\n<li>Performance is critical<\/li>\n\n\n\n<li>Your components have predictable props<\/li>\n\n\n\n<li>You want a &#8220;set it and forget it&#8221; solution<\/li>\n<\/ul>\n<\/li>\n\n\n\n<li><strong>Use the Runtime Approach when:<\/strong><strong><br><\/strong>\n<ul class=\"wp-block-list\">\n<li>You&#8217;re committed to Next.js<\/li>\n\n\n\n<li>You need full Light DOM access<\/li>\n\n\n\n<li>Your props are highly dynamic<\/li>\n\n\n\n<li>You&#8217;re okay with more complexity for more power<\/li>\n<\/ul>\n<\/li>\n<\/ul>\n\n\n\n<p>In practice, we&#8217;ve seen teams start with the compiler approach for its simplicity and broader compatibility, then selectively use the runtime approach for specific components that need its advanced features.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\" id=\"h-the-plot-thickens-more-challenges\"><strong>The Plot Thickens: More Challenges<\/strong><\/h2>\n\n\n\n<h3 class=\"wp-block-heading\" id=\"h-the-bloat-monster\"><strong>The Bloat Monster<\/strong><\/h3>\n\n\n\n<p>Remember how we embed styles in each component&#8217;s Declarative Shadow DOM? Well, imagine a button component used 50 times on a page. That&#8217;s the same styles repeated 50 times. Your HTML document becomes chonkier than a sumo wrestler.<\/p>\n\n\n\n<p>Our workaround? Stencil&#8217;s &#8220;scoped components&#8221; \u2014 fake web components that transform into real ones on the client. It&#8217;s like shipping IKEA furniture instead of assembled pieces.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\" id=\"h-the-conditional-rendering-conundrum\"><strong>The Conditional Rendering Conundrum<\/strong><\/h3>\n\n\n\n<p>Complex components that render differently based on conditions or child nodes? That&#8217;s where things get spicy. We introduced build flags to help:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>render() {\n\n\u00a0\u00a0if (Build.isServer) {\n\n\u00a0\u00a0\u00a0\u00a0return &lt;div&gt;loading...&lt;\/div&gt;\n\n\u00a0\u00a0}\n\n\u00a0\u00a0return &lt;ul&gt;{\/* actual content *\/}&lt;\/ul&gt;\n\n}<\/code><\/pre>\n\n\n\n<p>It&#8217;s not elegant, but it works. Sometimes you need duct tape to hold things together.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\" id=\"h-the-verdict-are-we-there-yet\"><strong>The Verdict: Are We There Yet?<\/strong><\/h2>\n\n\n\n<p>So, are Web Components ready for prime-time SSR? Well&#8230; it&#8217;s complicated.<\/p>\n\n\n\n<p>The good news: We&#8217;ve made it work. Stencil now supports SSR in React and Vue environments. You can build design systems with Web Components and render them on the server.<\/p>\n\n\n\n<p>The reality check: It&#8217;s not seamless. Even lit.dev uses server-rendered Web Components sparingly. The overhead, complexity, and edge cases mean that for many applications, traditional framework components still make more sense.<\/p>\n\n\n\n<p>Web Components shine in environments where SSR doesn&#8217;t matter \u2014 Chrome&#8217;s internal pages, Electron apps, VS Code extensions. But for your typical Next.js e-commerce site? The jury&#8217;s still out.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\" id=\"h-the-journey-continues\"><strong>The Journey Continues<\/strong><\/h2>\n\n\n\n<p>The inability to elegantly handle SSR remains one of the biggest barriers to Web Components adoption. We&#8217;re making progress, but there&#8217;s still a mountain to climb.<\/p>\n\n\n\n<p>The web platform is evolving. Proposals like Declarative Custom Elements could eliminate many current pain points. Until then, we&#8217;ll keep pushing, experimenting, and occasionally pulling our hair out.<\/p>\n\n\n\n<p>Because that&#8217;s what we do. We&#8217;re developers. We solve problems, even when those problems involve making fish climb trees.<\/p>\n\n\n\n<p><em>Thanks for joining me on this journey through the SSR wilderness. May your hydration errors be few and your bundle sizes small.<\/em><\/p>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\" \/>\n\n\n\n<p><strong>About the Author<\/strong>: A Stencil framework developer who has spent way too much time thinking about Shadow DOMs and server environments. When not wrestling with SSR, can be found explaining why Web Components aren&#8217;t dead yet.<\/p>\n","protected":false},"excerpt":{"rendered":"<p>Picture this: You&#8217;ve built a beautiful design system using Web Components. Your components are framework-agnostic, encapsulated, and performant. Life is good&#8230; until someone asks, &#8220;But does it work with SSR?&#8221; Record scratch. Freeze frame. That&#8217;s where our story begins. The Promise Land: SSR and Web Components Before we dive into the trenches, let&#8217;s set the [&hellip;]<\/p>\n","protected":false},"author":106,"featured_media":6466,"comment_status":"open","ping_status":"open","sticky":false,"template":"","format":"standard","meta":{"_acf_changed":false,"publish_to_discourse":"1","publish_post_category":"21","wpdc_auto_publish_overridden":"","wpdc_topic_tags":"","wpdc_pin_topic":"","wpdc_pin_until":"","discourse_post_id":"586988","discourse_permalink":"http:\/\/forum.ionicframework.com\/t\/the-quest-for-ssr-with-web-components-a-stencil-developers-journey\/248302","wpdc_publishing_response":"success","wpdc_publishing_error":"","_jetpack_memberships_contains_paid_content":false,"footnotes":""},"categories":[1,122,223],"tags":[295,294,292,293,171,76],"class_list":["post-6465","post","type-post","status-publish","format-standard","has-post-thumbnail","hentry","category-all","category-product","category-stencil","tag-next","tag-rendering","tag-server","tag-side","tag-ssr","tag-stencil"],"acf":[],"yoast_head":"<!-- This site is optimized with the Yoast SEO Premium plugin v23.0 (Yoast SEO v23.0) - https:\/\/yoast.com\/wordpress\/plugins\/seo\/ -->\n<title>The Quest for SSR with Web Components: A Stencil Developer&#039;s Journey - Ionic Blog<\/title>\n<meta name=\"description\" content=\"Explore StencilJS SSR to enhance your Web Components with server-side rendering for better performance and SEO.\" \/>\n<meta name=\"robots\" content=\"index, follow, max-snippet:-1, max-image-preview:large, max-video-preview:-1\" \/>\n<link rel=\"canonical\" href=\"https:\/\/ionic.io\/blog\/the-quest-for-ssr-with-web-components-a-stencil-developers-journey\" \/>\n<meta property=\"og:locale\" content=\"en_US\" \/>\n<meta property=\"og:type\" content=\"article\" \/>\n<meta property=\"og:title\" content=\"The Quest for SSR with Web Components: A Stencil Developer&#039;s Journey\" \/>\n<meta property=\"og:description\" content=\"Explore how StencilJS SSR enhances Web Components with server-side rendering for faster loads and better SEO.\" \/>\n<meta property=\"og:url\" content=\"https:\/\/ionic.io\/blog\/the-quest-for-ssr-with-web-components-a-stencil-developers-journey\" \/>\n<meta property=\"og:site_name\" content=\"Ionic Blog\" \/>\n<meta property=\"article:published_time\" content=\"2025-06-25T04:54:25+00:00\" \/>\n<meta property=\"article:modified_time\" content=\"2025-06-25T05:05:59+00:00\" \/>\n<meta property=\"og:image\" content=\"https:\/\/ionic.io\/blog\/wp-content\/uploads\/2025\/06\/blog-1024x683.png\" \/>\n\t<meta property=\"og:image:width\" content=\"1024\" \/>\n\t<meta property=\"og:image:height\" content=\"683\" \/>\n\t<meta property=\"og:image:type\" content=\"image\/png\" \/>\n<meta name=\"author\" content=\"Stencil Team\" \/>\n<meta name=\"twitter:card\" content=\"summary_large_image\" \/>\n<meta name=\"twitter:creator\" content=\"@ionicframework\" \/>\n<meta name=\"twitter:site\" content=\"@ionicframework\" \/>\n<meta name=\"twitter:label1\" content=\"Written by\" \/>\n\t<meta name=\"twitter:data1\" content=\"Stencil Team\" \/>\n\t<meta name=\"twitter:label2\" content=\"Est. reading time\" \/>\n\t<meta name=\"twitter:data2\" content=\"6 minutes\" \/>\n<script type=\"application\/ld+json\" class=\"yoast-schema-graph\">{\"@context\":\"https:\/\/schema.org\",\"@graph\":[{\"@type\":\"Article\",\"@id\":\"https:\/\/ionic.io\/blog\/the-quest-for-ssr-with-web-components-a-stencil-developers-journey#article\",\"isPartOf\":{\"@id\":\"https:\/\/ionic.io\/blog\/the-quest-for-ssr-with-web-components-a-stencil-developers-journey\"},\"author\":{\"name\":\"Stencil Team\",\"@id\":\"https:\/\/ionic.io\/blog\/#\/schema\/person\/82fcd9aa19604364f3c30a946de8e646\"},\"headline\":\"The Quest for SSR with Web Components: A Stencil Developer&#8217;s Journey\",\"datePublished\":\"2025-06-25T04:54:25+00:00\",\"dateModified\":\"2025-06-25T05:05:59+00:00\",\"mainEntityOfPage\":{\"@id\":\"https:\/\/ionic.io\/blog\/the-quest-for-ssr-with-web-components-a-stencil-developers-journey\"},\"wordCount\":1360,\"commentCount\":0,\"publisher\":{\"@id\":\"https:\/\/ionic.io\/blog\/#organization\"},\"image\":{\"@id\":\"https:\/\/ionic.io\/blog\/the-quest-for-ssr-with-web-components-a-stencil-developers-journey#primaryimage\"},\"thumbnailUrl\":\"https:\/\/ionic.io\/blog\/wp-content\/uploads\/2025\/06\/blog.png\",\"keywords\":[\"next\",\"rendering\",\"server\",\"side\",\"SSR\",\"stencil\"],\"articleSection\":[\"All\",\"Product\",\"Stencil\"],\"inLanguage\":\"en-US\",\"potentialAction\":[{\"@type\":\"CommentAction\",\"name\":\"Comment\",\"target\":[\"https:\/\/ionic.io\/blog\/the-quest-for-ssr-with-web-components-a-stencil-developers-journey#respond\"]}]},{\"@type\":\"WebPage\",\"@id\":\"https:\/\/ionic.io\/blog\/the-quest-for-ssr-with-web-components-a-stencil-developers-journey\",\"url\":\"https:\/\/ionic.io\/blog\/the-quest-for-ssr-with-web-components-a-stencil-developers-journey\",\"name\":\"The Quest for SSR with Web Components: A Stencil Developer's Journey - Ionic Blog\",\"isPartOf\":{\"@id\":\"https:\/\/ionic.io\/blog\/#website\"},\"primaryImageOfPage\":{\"@id\":\"https:\/\/ionic.io\/blog\/the-quest-for-ssr-with-web-components-a-stencil-developers-journey#primaryimage\"},\"image\":{\"@id\":\"https:\/\/ionic.io\/blog\/the-quest-for-ssr-with-web-components-a-stencil-developers-journey#primaryimage\"},\"thumbnailUrl\":\"https:\/\/ionic.io\/blog\/wp-content\/uploads\/2025\/06\/blog.png\",\"datePublished\":\"2025-06-25T04:54:25+00:00\",\"dateModified\":\"2025-06-25T05:05:59+00:00\",\"description\":\"Explore StencilJS SSR to enhance your Web Components with server-side rendering for better performance and SEO.\",\"breadcrumb\":{\"@id\":\"https:\/\/ionic.io\/blog\/the-quest-for-ssr-with-web-components-a-stencil-developers-journey#breadcrumb\"},\"inLanguage\":\"en-US\",\"potentialAction\":[{\"@type\":\"ReadAction\",\"target\":[\"https:\/\/ionic.io\/blog\/the-quest-for-ssr-with-web-components-a-stencil-developers-journey\"]}]},{\"@type\":\"ImageObject\",\"inLanguage\":\"en-US\",\"@id\":\"https:\/\/ionic.io\/blog\/the-quest-for-ssr-with-web-components-a-stencil-developers-journey#primaryimage\",\"url\":\"https:\/\/ionic.io\/blog\/wp-content\/uploads\/2025\/06\/blog.png\",\"contentUrl\":\"https:\/\/ionic.io\/blog\/wp-content\/uploads\/2025\/06\/blog.png\",\"width\":1536,\"height\":1024},{\"@type\":\"BreadcrumbList\",\"@id\":\"https:\/\/ionic.io\/blog\/the-quest-for-ssr-with-web-components-a-stencil-developers-journey#breadcrumb\",\"itemListElement\":[{\"@type\":\"ListItem\",\"position\":1,\"name\":\"Home\",\"item\":\"https:\/\/ionic.io\/blog\"},{\"@type\":\"ListItem\",\"position\":2,\"name\":\"The Quest for SSR with Web Components: A Stencil Developer&#8217;s Journey\"}]},{\"@type\":\"WebSite\",\"@id\":\"https:\/\/ionic.io\/blog\/#website\",\"url\":\"https:\/\/ionic.io\/blog\/\",\"name\":\"ionic.io\/blog\",\"description\":\"Build amazing native and progressive web apps with the web\",\"publisher\":{\"@id\":\"https:\/\/ionic.io\/blog\/#organization\"},\"potentialAction\":[{\"@type\":\"SearchAction\",\"target\":{\"@type\":\"EntryPoint\",\"urlTemplate\":\"https:\/\/ionic.io\/blog\/?s={search_term_string}\"},\"query-input\":\"required name=search_term_string\"}],\"inLanguage\":\"en-US\"},{\"@type\":\"Organization\",\"@id\":\"https:\/\/ionic.io\/blog\/#organization\",\"name\":\"Ionic\",\"url\":\"https:\/\/ionic.io\/blog\/\",\"logo\":{\"@type\":\"ImageObject\",\"inLanguage\":\"en-US\",\"@id\":\"https:\/\/ionic.io\/blog\/#\/schema\/logo\/image\/\",\"url\":\"https:\/\/ionic.io\/blog\/wp-content\/uploads\/2020\/10\/white-on-color.png\",\"contentUrl\":\"https:\/\/ionic.io\/blog\/wp-content\/uploads\/2020\/10\/white-on-color.png\",\"width\":1920,\"height\":854,\"caption\":\"Ionic\"},\"image\":{\"@id\":\"https:\/\/ionic.io\/blog\/#\/schema\/logo\/image\/\"},\"sameAs\":[\"https:\/\/x.com\/ionicframework\"]},{\"@type\":\"Person\",\"@id\":\"https:\/\/ionic.io\/blog\/#\/schema\/person\/82fcd9aa19604364f3c30a946de8e646\",\"name\":\"Stencil Team\",\"image\":{\"@type\":\"ImageObject\",\"inLanguage\":\"en-US\",\"@id\":\"https:\/\/ionic.io\/blog\/#\/schema\/person\/image\/\",\"url\":\"https:\/\/ionic.io\/blog\/wp-content\/uploads\/2023\/05\/stencil-white-on-color-150x150.png\",\"contentUrl\":\"https:\/\/ionic.io\/blog\/wp-content\/uploads\/2023\/05\/stencil-white-on-color-150x150.png\",\"caption\":\"Stencil Team\"},\"url\":\"https:\/\/ionic.io\/blog\/author\/stencil\"}]}<\/script>\n<!-- \/ Yoast SEO Premium plugin. -->","yoast_head_json":{"title":"The Quest for SSR with Web Components: A Stencil Developer's Journey - Ionic Blog","description":"Explore StencilJS SSR to enhance your Web Components with server-side rendering for better performance and SEO.","robots":{"index":"index","follow":"follow","max-snippet":"max-snippet:-1","max-image-preview":"max-image-preview:large","max-video-preview":"max-video-preview:-1"},"canonical":"https:\/\/ionic.io\/blog\/the-quest-for-ssr-with-web-components-a-stencil-developers-journey","og_locale":"en_US","og_type":"article","og_title":"The Quest for SSR with Web Components: A Stencil Developer's Journey","og_description":"Explore how StencilJS SSR enhances Web Components with server-side rendering for faster loads and better SEO.","og_url":"https:\/\/ionic.io\/blog\/the-quest-for-ssr-with-web-components-a-stencil-developers-journey","og_site_name":"Ionic Blog","article_published_time":"2025-06-25T04:54:25+00:00","article_modified_time":"2025-06-25T05:05:59+00:00","og_image":[{"width":1024,"height":683,"url":"https:\/\/ionic.io\/blog\/wp-content\/uploads\/2025\/06\/blog-1024x683.png","type":"image\/png"}],"author":"Stencil Team","twitter_card":"summary_large_image","twitter_creator":"@ionicframework","twitter_site":"@ionicframework","twitter_misc":{"Written by":"Stencil Team","Est. reading time":"6 minutes"},"schema":{"@context":"https:\/\/schema.org","@graph":[{"@type":"Article","@id":"https:\/\/ionic.io\/blog\/the-quest-for-ssr-with-web-components-a-stencil-developers-journey#article","isPartOf":{"@id":"https:\/\/ionic.io\/blog\/the-quest-for-ssr-with-web-components-a-stencil-developers-journey"},"author":{"name":"Stencil Team","@id":"https:\/\/ionic.io\/blog\/#\/schema\/person\/82fcd9aa19604364f3c30a946de8e646"},"headline":"The Quest for SSR with Web Components: A Stencil Developer&#8217;s Journey","datePublished":"2025-06-25T04:54:25+00:00","dateModified":"2025-06-25T05:05:59+00:00","mainEntityOfPage":{"@id":"https:\/\/ionic.io\/blog\/the-quest-for-ssr-with-web-components-a-stencil-developers-journey"},"wordCount":1360,"commentCount":0,"publisher":{"@id":"https:\/\/ionic.io\/blog\/#organization"},"image":{"@id":"https:\/\/ionic.io\/blog\/the-quest-for-ssr-with-web-components-a-stencil-developers-journey#primaryimage"},"thumbnailUrl":"https:\/\/ionic.io\/blog\/wp-content\/uploads\/2025\/06\/blog.png","keywords":["next","rendering","server","side","SSR","stencil"],"articleSection":["All","Product","Stencil"],"inLanguage":"en-US","potentialAction":[{"@type":"CommentAction","name":"Comment","target":["https:\/\/ionic.io\/blog\/the-quest-for-ssr-with-web-components-a-stencil-developers-journey#respond"]}]},{"@type":"WebPage","@id":"https:\/\/ionic.io\/blog\/the-quest-for-ssr-with-web-components-a-stencil-developers-journey","url":"https:\/\/ionic.io\/blog\/the-quest-for-ssr-with-web-components-a-stencil-developers-journey","name":"The Quest for SSR with Web Components: A Stencil Developer's Journey - Ionic Blog","isPartOf":{"@id":"https:\/\/ionic.io\/blog\/#website"},"primaryImageOfPage":{"@id":"https:\/\/ionic.io\/blog\/the-quest-for-ssr-with-web-components-a-stencil-developers-journey#primaryimage"},"image":{"@id":"https:\/\/ionic.io\/blog\/the-quest-for-ssr-with-web-components-a-stencil-developers-journey#primaryimage"},"thumbnailUrl":"https:\/\/ionic.io\/blog\/wp-content\/uploads\/2025\/06\/blog.png","datePublished":"2025-06-25T04:54:25+00:00","dateModified":"2025-06-25T05:05:59+00:00","description":"Explore StencilJS SSR to enhance your Web Components with server-side rendering for better performance and SEO.","breadcrumb":{"@id":"https:\/\/ionic.io\/blog\/the-quest-for-ssr-with-web-components-a-stencil-developers-journey#breadcrumb"},"inLanguage":"en-US","potentialAction":[{"@type":"ReadAction","target":["https:\/\/ionic.io\/blog\/the-quest-for-ssr-with-web-components-a-stencil-developers-journey"]}]},{"@type":"ImageObject","inLanguage":"en-US","@id":"https:\/\/ionic.io\/blog\/the-quest-for-ssr-with-web-components-a-stencil-developers-journey#primaryimage","url":"https:\/\/ionic.io\/blog\/wp-content\/uploads\/2025\/06\/blog.png","contentUrl":"https:\/\/ionic.io\/blog\/wp-content\/uploads\/2025\/06\/blog.png","width":1536,"height":1024},{"@type":"BreadcrumbList","@id":"https:\/\/ionic.io\/blog\/the-quest-for-ssr-with-web-components-a-stencil-developers-journey#breadcrumb","itemListElement":[{"@type":"ListItem","position":1,"name":"Home","item":"https:\/\/ionic.io\/blog"},{"@type":"ListItem","position":2,"name":"The Quest for SSR with Web Components: A Stencil Developer&#8217;s Journey"}]},{"@type":"WebSite","@id":"https:\/\/ionic.io\/blog\/#website","url":"https:\/\/ionic.io\/blog\/","name":"ionic.io\/blog","description":"Build amazing native and progressive web apps with the web","publisher":{"@id":"https:\/\/ionic.io\/blog\/#organization"},"potentialAction":[{"@type":"SearchAction","target":{"@type":"EntryPoint","urlTemplate":"https:\/\/ionic.io\/blog\/?s={search_term_string}"},"query-input":"required name=search_term_string"}],"inLanguage":"en-US"},{"@type":"Organization","@id":"https:\/\/ionic.io\/blog\/#organization","name":"Ionic","url":"https:\/\/ionic.io\/blog\/","logo":{"@type":"ImageObject","inLanguage":"en-US","@id":"https:\/\/ionic.io\/blog\/#\/schema\/logo\/image\/","url":"https:\/\/ionic.io\/blog\/wp-content\/uploads\/2020\/10\/white-on-color.png","contentUrl":"https:\/\/ionic.io\/blog\/wp-content\/uploads\/2020\/10\/white-on-color.png","width":1920,"height":854,"caption":"Ionic"},"image":{"@id":"https:\/\/ionic.io\/blog\/#\/schema\/logo\/image\/"},"sameAs":["https:\/\/x.com\/ionicframework"]},{"@type":"Person","@id":"https:\/\/ionic.io\/blog\/#\/schema\/person\/82fcd9aa19604364f3c30a946de8e646","name":"Stencil Team","image":{"@type":"ImageObject","inLanguage":"en-US","@id":"https:\/\/ionic.io\/blog\/#\/schema\/person\/image\/","url":"https:\/\/ionic.io\/blog\/wp-content\/uploads\/2023\/05\/stencil-white-on-color-150x150.png","contentUrl":"https:\/\/ionic.io\/blog\/wp-content\/uploads\/2023\/05\/stencil-white-on-color-150x150.png","caption":"Stencil Team"},"url":"https:\/\/ionic.io\/blog\/author\/stencil"}]}},"jetpack_sharing_enabled":true,"jetpack_featured_media_url":"https:\/\/ionic.io\/blog\/wp-content\/uploads\/2025\/06\/blog.png","_links":{"self":[{"href":"https:\/\/ionic.io\/blog\/wp-json\/wp\/v2\/posts\/6465","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/ionic.io\/blog\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/ionic.io\/blog\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/ionic.io\/blog\/wp-json\/wp\/v2\/users\/106"}],"replies":[{"embeddable":true,"href":"https:\/\/ionic.io\/blog\/wp-json\/wp\/v2\/comments?post=6465"}],"version-history":[{"count":2,"href":"https:\/\/ionic.io\/blog\/wp-json\/wp\/v2\/posts\/6465\/revisions"}],"predecessor-version":[{"id":6517,"href":"https:\/\/ionic.io\/blog\/wp-json\/wp\/v2\/posts\/6465\/revisions\/6517"}],"wp:featuredmedia":[{"embeddable":true,"href":"https:\/\/ionic.io\/blog\/wp-json\/wp\/v2\/media\/6466"}],"wp:attachment":[{"href":"https:\/\/ionic.io\/blog\/wp-json\/wp\/v2\/media?parent=6465"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/ionic.io\/blog\/wp-json\/wp\/v2\/categories?post=6465"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/ionic.io\/blog\/wp-json\/wp\/v2\/tags?post=6465"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}