Skip to content

Instantly share code, notes, and snippets.

@arnu515
Last active January 11, 2025 03:36
Show Gist options
  • Save arnu515/f5d3765d580915f288364eb4d91da56e to your computer and use it in GitHub Desktop.
Save arnu515/f5d3765d580915f288364eb4d91da56e to your computer and use it in GitHub Desktop.
Streaming SSR with Bun + Elysia and TailwindCSS

Streaming SSR with Bun + Elysia and TailwindCSS

This is a simple example on how to stream HTML with Elysia in Bun with TailwindCSS (through twind).

This uses the latest @twind/core API.

Please suggest any speed improvements in the comments 🙏

TODO

  • Delete the style tag of old fallback elements after they leave the DOM. (Need help with that).
    Currently the <head> gets cluttered with unused styles.
    image
import { Elysia } from "elysia"
import { html } from "@elysiajs/html"
import { renderToStream, Suspense } from "@kitajs/html/suspense"
import { getStyles, tw, tx, withTwind } from "./tw"
import { Readable } from "stream"
const renderAsyncEl = (rid: number) => {
return (
<>
<Suspense
fallback={<h3 class="text-green-500">loading joke...</h3>}
rid={rid}
catch={e => (
<h3 class="text-red-500" safe>
{(e as Error).message}
</h3>
)}
>
<FetchJoke />
</Suspense>
<Suspense
fallback={<h3 class="text-green-500">loading quote...</h3>}
rid={rid}
catch={e => (
<h3 class="text-red-500" safe>
{(e as Error).message}
</h3>
)}
>
<FetchQuote />
</Suspense>
</>
)
}
const FetchJoke = async () => {
const res = await fetch(
"https://v2.jokeapi.dev/joke/Programming?type=single&amount=5"
)
const data = await res.json()
if (!res.ok || !Array.isArray(data.jokes) || data.jokes.length !== 5) {
throw new Error("Failed to fetch joke")
}
return (
<ol class="bg-yellow-500 px-8 py-4 list-number">
{data.jokes.map(({ joke }: { joke: string }) => <li safe>{joke}</li>) as "safe"}
</ol>
)
}
const FetchQuote = async () => {
const res = await fetch("https://zenquotes.io/api/random")
const data = await res.json()
if (!res.ok || !Array.isArray(data) || data.length !== 1) {
throw new Error("Failed to fetch quote")
}
return (
<blockquote class="text-2xl p-4 text-center bg-blue-500 text-white mt-4">
<p safe>{data[0].q}</p>
<footer class="text-right text-sm">
<cite safe>{data[0].a}</cite>
</footer>
</blockquote>
)
}
const app = new Elysia()
.use(html())
.get("/", () => {
tw.clear()
const html = <h1 class="text-center text-5xl font-bold my-4">Hello, world!</h1>
return withTwind(`
<!DOCTYPE html>
<html>
<head><title>Synchronous HTML!</title></head>
<body>${html}</body>
</html>
`)
})
.get("/async", async () => {
const html = Readable.toWeb(
renderToStream(renderAsyncEl) // Renders asynchronous JSX to a stream.readable
) // Converts stream.readable to web ReadableStream. Readable.toWeb is experimental
let recvFirstChunk = false
const st = html.pipeThrough(
new TransformStream({
start() { },
transform(chunk, controller) {
if (!recvFirstChunk) {
tw.clear()
const html = new TextDecoder("utf-8").decode(chunk)
const shell = `<!DOCTYPE html><html><head><title>Asynchronous HTML!</title></head><body>${html}</body></html>`
controller.enqueue(new TextEncoder("utf-8").encode(withTwind(shell)))
recvFirstChunk = true
} else {
// Do not send HTML shell, it is already present.
let { html, css } = getStyles(new TextDecoder("utf-8").decode(chunk))
// Only oppend CSS if it exists
if (!css.trim()) {
controller.enqueue(new TextEncoder("utf-8").encode(html))
} else {
// Use javascript to append the styles to the head
css = JSON.stringify(css)
const hash = Bun.hash(chunk)
html += `<script id="ssr__sas-${hash}">var s=document.createElement('style');s.innerText=${css};document.head.appendChild(s);document.querySelector("#ssr__sas-${hash}").remove()</script>`
controller.enqueue(new TextEncoder("utf-8").encode(html))
}
}
}
})
)
return new Response(st, {
headers: { "Content-Type": "text/html; charset=utf-8" }
})
})
.listen(3000)
console.log(`🦊 Elysia is running at ${app.server?.hostname}:${app.server?.port}`)
import {
defineConfig,
extract,
injectGlobal as injectGlobal$,
twind,
tx as tx$,
virtual
} from "@twind/core"
import pAutoprefix from "@twind/preset-autoprefix"
import pTailwind from "@twind/preset-tailwind"
import pExt from "@twind/preset-ext"
import pContainerQueries from "@twind/preset-container-queries"
export const config = defineConfig({
presets: [pAutoprefix(), pTailwind(), pExt(), pContainerQueries()]
})
export const sheet = virtual()
export const tw = twind(config, sheet)
export const tx = tx$.bind(tw)
export const injectGlobal = injectGlobal$.bind(tw)
export function getStyles(html: string) {
return extract(html, tw)
}
export function withTwind(html: string) {
const { html: newHTML, css } = getStyles(html)
return newHTML.replace("</head>", `<style>${css}</style></head>`)
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment