Skip to content

Instantly share code, notes, and snippets.

@KonnorRogers
Created November 21, 2025 17:36
Show Gist options
  • Select an option

  • Save KonnorRogers/df194128d6b78f9d456ba09792ae1816 to your computer and use it in GitHub Desktop.

Select an option

Save KonnorRogers/df194128d6b78f9d456ba09792ae1816 to your computer and use it in GitHub Desktop.
server + client rendered lit

SSR -> Client Render

There's a few approaches.

We can take the React / Vue / et al approach:

Server

import { collectResult } from "@lit-labs/ssr/lib/render-result.js"
import { render } from "lit"
import App from "./app.js"

app.get("/", async (req, res, next) => {
  const app = await collectResult(render(new App()))
  res.send(`
    <!DOCTYPE html>
    <html>
      <head></head>
      <body>
        <div id="app">${app}</div>
      </body>
    </html>
  `)
})

Client

import App from "./app.js"
import {render} from "lit"

render(new App(), document.querySelector("#app"))

This approach is pretty tried and true, but does require both server + client are loading the exact same tree. If you plan to "sprinkle" pieces of Lit across a traditional "server rendered" app, this approach isn't as nice as it fully "takes over" rendering and everything must now be done with Lit.

Pros

  • Very easy to implement
  • Tried and true

Cons

  • Requires to "own" the entire tree its rendering so you can't selectively add bits, unless you add specific "mounts"

"Component" rendering

What if instead, we wanted to "sprinkle" pieces of Lit, almost like an "island" architecture?

Server

import { collectResult } from "@lit-labs/ssr/lib/render-result.js"
import { render } from "lit"
import Foo from "./foo.js"
import Bar from "./bar.js"

app.get("/", async (req, res, next) => {
  const foo = await collectResult(render(new App()))
  const bar = await collectResult(render(new App()))
  res.send(`
    <!DOCTYPE html>
    <html>
      <head></head>
      <body>
        <div data-component="foo">
          ${foo}
        </div>
        <div data-component="bar">
          ${bar}
        </div>
      </body>
    </html>
  `)
})

Client

import Watcher from "watcher"
import Foo from "./foo.js"
import Bar from "./bar.js"

const watcher = new Watcher()
  .register("foo", Foo)
  .register("bar", Bar)

// Pretend we do this in a MutationObserver and check if we've already rendered the component.
document.querySelectorAll("[data-component]").forEach((el) => {
  const componentName = el.dataset.component

  if (watcher.has(component)) {
    hydrate(watcher.get(component), el)
  }
})

Pros

  • Lets you selectively use Lit and not need to rewrite your whole rendering process. This is essentially like an "islands" architecture

Cons

  • A component cannot affect its "host" container
  • Things are a little "decoupled" when used this way.

The "bananas" idea

What if you could insert a Lit hydrated element anywhere

import { collectResult } from "@lit-labs/ssr/lib/render-result.js"
import { render } from "lit"
import App from "./app.js"

app.get("/", async (req, res, next) => {
  const html = await collectResult(render(new Layout()))
  res.send(html)
})

And imagine Layout has pieces rendered in the <head> / <body> and can either have static html bits, or nested components.

On the client, the way I expect this would work, is each "Component" is assigned a unique identifier and a comment node is injected above it. We would essentially "walk a tree" and hydrate + render every known identifier + comment node, and dump a big ole JSON payload for each unique identifier + component. like this:

{
  "Layout-134422234": {},
  "App-145242": {},
  "Counter-134282": {state: {count: 50}}
  "Counter-928492": {state: {count: 100}}
}

and this is stored on a Map() that when a TreeWalker comes across the node, can provide the expected state.

The challenge with this is obviously nested trees and how does it work when an outer node re-renders...does it do granular updates of inner nodes? I don't know. What about data fetching in the component? How does that work?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment