{
"$type": "site.standard.document",
"bskyPostRef": {
"cid": "bafyreifuvifnslgmtgz4bd3lsv47yf2ilyqwz3wzrooucr44jgxpyfzel4",
"uri": "at://did:plc:vrrdgcidwpvn4omvn7uuufoo/app.bsky.feed.post/3lczxybjvlk25"
},
"coverImage": {
"$type": "blob",
"ref": {
"$link": "bafkreif5llk7t7u6viuca5j77pqclx7hr57nkjpowxjwnv3rm6nd2hcjqu"
},
"mimeType": "image/png",
"size": 34228
},
"description": "The lack of server-side rendering in web components has become a sort of folk belief that oft goes unquestioned. I am happy to report that the fears are unfounded.",
"path": "/words/isomorphic-web-components/",
"publishedAt": "2024-12-11T00:00:00Z",
"site": "at://did:plc:vrrdgcidwpvn4omvn7uuufoo/site.standard.publication/3mmyfl3pxzi2a",
"tags": [
"webcomponents"
],
"textContent": "Web components might be great, if only you could render them on the server.\n\nOr can you?\nThe lack of server-side rendering has become a sort of folk belief that oft goes unquestioned, and many people form opinions based on this (alleged) missing feature.\n\nWell, I am happy to report that the fears are unfounded: you can absolutely server-side render a web component.\nBut there are a few different ways it can go down.\n\nThe Current Landscape\n\nLet's start from the top.\nThe building blocks of web components — template elements, custom elements and (declarative) shadow DOM — are all just HTML tags.\nSo from a pedantic point of view, server-side rendering a web component is trivial: just put a <template> or a <custom-element> tag in your markup.\n\nI'm being glib, but this already genuinely powerful!\nCustom elements let you attach logic to specific points in the light DOM.\nRather than declaring that attachment point in a separate JavaScript file, though, you can do it directly in your HTML markup.\nThis strategy — using custom elements without templates or shadow DOM, to enhance light DOM elements that already exist — has come to be called HTML web components.\n\nYou don't even need to bring in JavaScript for web components to be useful.\nHawk Ticehurst shared a pattern he calls CSS web components, in which custom element attributes are used as hooks for CSS selectors.\nThis gives us a props-like API to modify a component's appearance without writing a byte of JavaScript.\n\nNone of this is what people usually mean, though.\n\"Web component\" is really just an umbrella term for those three APIs, but in practice people use it to mean adding client-side behavior to custom elements by subclassing HTMLElement.\nAnd when they talk about server-side rendering web components, they mean running that subclass on the server and having it spit out the markup it would generate on the client.\n\nThis approach — running the same code in two different environments — is popularly called isomorphism.\nIt's how most major JavaScript frameworks approach server-side rendering.\nBut for whatever reason, resources for doing it with web components are few and far between.\n\nIs isomorphism purely a JavaScript framework thing, or is there a more standard way to do it?\n\nIf you open the HTML custom elements spec and search for the word \"server\", you'll get two results and both of them are about form processing.\nYou won't find it at all in the DOM spec.\n\nYou might be thinking, \"wait a minute — isn't declarative shadow DOM a spec for server-side rendering web components?\"\nThe answer to that is: not really.\nDeclarative shadow DOM defines a way to set up shadow roots within HTML (i.e. without JavaScript).\n\nBut many web components don't use shadow DOM at all — they render plain old light DOM.\nAnd while the spec details how HTML is parsed into a shadow root, it's agnostic as to how that HTML gets generated.\n\nThat makes sense, though!\nAt the risk of stating the obvious: browser specs are written for the browser.\nTheir concern is how the browser interprets the HTML it receives; how that HTML is created is outside of their purview.\n\nOkay, no official guidance from the W3C.\nNow what?\n\nAt this point, libraries like Lit, WebC and Enhance enter the discussion.\nThese are tools that target web components as an output format: you use them to build a component, and at the end you get a custom element that you can use in any website or web app.\nMany of them also let you render that component to static HTML on the server.\nCase closed, right?\n\nNot quite.\nThe code you write in these libraries might not look much like \"vanilla\" web components at all.\nHere's an example of an Enhance element:\n\nYou'd be forgiven for thinking you were looking at a React component!\nFrankly, I don't see a fundamental difference between these and frameworks like Svelte or Vue, which can also use web components as a compile target.\n\nTo be clear, I don't mean any of this as a slight.\nThese are all good tools — web component libraries and JavaScript frameworks both.\nUltimately, they all:\n\nLet you build server-side renderable web components, if you…\n\nUse their custom APIs and/or languages, and…\n\nInclude them as a dependency in your project.\n\nThat makes me uneasy.\nWhen I choose to write a web component rather than, say, a Svelte component, one of my main reasons is to work directly with the web platform. I don’t want to add a build step or change how I write my code.\n\nOne of the coolest things about web components is that they act as a decoupling layer.\nIt doesn't matter whether a component is built with Enhance, Lit or anything else; I can drop it into a Svelte app or an Astro site or a Markdown file or a page of handwritten HTML and it will Just Work and I will be none the wiser.\n\nWhich is why I'm not super thrilled about server-side rendering solutions that are tied to particular libraries.\nThe interoperability promise is broken — or, at the very least, weakened.\nHow a component is built is no longer simply an implementation detail.\nWhat I've chosen for the frontend now exerts influence on the backend, and vice versa.\n\nSo the goal is to take existing web components and render them server-side:\n\nStart with a web component that already works in the browser.\n\nThis component will stand the test of time!\nNo dependencies to update, no servers to maintain, no build processes that might inexplicably stop working.\nSure, there are reasons why JavaScript might break, but generally speaking this component will reliably work forever.\n\nRender that component into the HTML delivered to the browser.\nThis introduces a dependency and a build process that can break, but that's okay — if worse comes to worst, the component can still work with only client-side rendering.\n\nThis is kind of like a bizarro progressive enhancement.\nRather than starting with HTML and enhancing it with JavaScript, we're starting with JavaScript and enhancing it with HTML.\n\nNote that this is not mutually exclusive with traditional progressive enhancement!\nWith this strategy, the component's server-side rendered HTML can still deliver baseline functionality even if the JavaScript fails to load.\nThe fact that the same code that generated the HTML later ends up running in the browser is an implementation detail.\n\nCuriously, there doesn't seem to be much written online about this approach.\nIf people are doing it, they're not really talking about it.\nThat's why I decided to build a proof of concept.\n\nIsomorphic Rendering\n\nWe'll start by trying to server-side render this web component:\n\nWe want to be able to write this in our HTML:\n\n…and have it expand into this:\n\nIf we're going to take any web component that works on the client, that means we'll need a way to emulate the DOM.\nThere are a bunch of libraries that do this, but we'll use one called Happy DOM.\n\nWith Happy DOM in our toolbelt, the code to actually do the rendering is pretty short:\n\nAt the module's top level, we create a Happy DOM Window.\nJust like in a browser, an instance of Window contains all the global variables available — HTMLElement, customElements, you name it.\nWe'll take these global variables and set them on Node's global object, which makes them available to all modules we might import.\n\nWe only need to define one function to make this work.\nWe'll call it render, and it'll take two props: the HTML to render as a string, and an array of functions that import the web component classes.\nAfter awaiting the return value of each of those functions, we set the window's documentElement.innerHTML to the string we passed into the render function, then serialize the emulated DOM to an HTML string and return it.\n\nWe call the render function like this:\n\nThe important part here is that we need to import the render function before we import the web components.\nThat way, by the time the web components are declared, all the browser APIs they rely on are already available on Node's global scope.\n\nThat works for web components that stay within the light DOM.\nWhat about the shadow DOM?\n\nLet's update our component:\n\nIf you've used shadow DOM before, this probably looks familiar.\nOne thing that may be new to you — or at least, it was to me — is the serializable property, which instructs the element to render the shadow root into HTML.\n\nNow, if we put this in our markup:\n\n…it'll expand into this:\n\nThere you go: isomorphic web components.\n\nConclusion\n\nAny way you cut it, you can server-side render web components today:\n\nNo special tooling is required to use templates, custom elements and declarative shadow DOM\n\nWeb component libraries like Lit, Enhance and WebC let you write code that can both be server-side rendered and compiled to client-side web components\n\nEmulating the DOM lets you write isomorphic web components that work both on the server and in the browser\n\nSmart people can disagree about the best approach, but I'm partial to isomorphic rendering.\nIt works with all web components, no matter how they're written.\nIt fully embraces web platform APIs, rather than treating them as a compile target.\nAnd it makes our components resilient to toolchain entropy by gracefully degrading to client-side rendering.\n\nEven if you don't agree with me, though, there's a server-side rendering solution out there for you.\nThat's the nice thing about the web: it's flexible like that.",
"title": "Isomorphic Web Components"
}