← back to index

06. streaming SSR with DPU

The entire page is server-rendered in one HTTP response. The shell flushes immediately, then each section streams in as its backend resolves. Out-of-order. Zero client JS. Refresh as many times as you want.

feed

shop

waiting for footer...

learn more

source

Everything the server runs for this example. Each file links to GitHub so you can copy or fork it.

handler.ts/app/src/examples/06-ssr/handler.ts
import { sleep, streamingResponse } from "../../lib/streaming.ts";
import { loadShell, tryStatic } from "../../lib/files.ts";

const SHELL = loadShell(import.meta.url);
const SUFFIX = `</body></html>`;

interface Section {
  name: string;
  delayMs: number;
  html: string;
}

const SECTIONS: Section[] = [
  {
    name: "user",
    delayMs: 200,
    html: `<h2 style="margin:0 0 .25rem;">Hello, Paul</h2>
<p style="margin:0;color:var(--muted);font-size:.9rem;">last seen 2 minutes ago</p>`,
  },
  {
    name: "feed",
    delayMs: 900,
    html: `<ul style="margin:0;padding-left:1.2rem;">
  <li>declarative partial updates landed in chrome 148</li>
  <li>navigation api shipped in safari 26.2</li>
  <li>llms can now write whole browsers, apparently</li>
</ul>`,
  },
  {
    name: "shop",
    delayMs: 1600,
    html: `<div style="display:flex;gap:1rem;align-items:center;">
  <div style="width:48px;height:48px;border-radius:8px;background:var(--code-bg);display:flex;align-items:center;justify-content:center;font-weight:700;color:var(--muted);">PK</div>
  <div>
    <strong>kit · hire me</strong><br>
    <span style="color:var(--muted);font-size:.85rem;">£99/hr · 4.9 reviews</span>
  </div>
</div>`,
  },
  {
    name: "footer",
    delayMs: 2200,
    html:
      `<p style="margin:0;color:var(--muted);font-size:.8rem;">all sections rendered server-side, streamed as each backend completed. zero client JS.</p>`,
  },
];

export default function handle(_req: Request, path: string): Response | Promise<Response> {
  if (path === "/" || path === "/index.html") {
    return streamingResponse(async (write) => {
      await write(SHELL);
      const pending = SECTIONS.map((s) =>
        sleep(s.delayMs).then(() => write(`<template for="${s.name}">${s.html}</template>`))
      );
      await Promise.all(pending);
      await write(SUFFIX);
    });
  }
  return tryStatic(import.meta.url, path) ?? new Response("Not found", { status: 404 });
}

open on GitHub →

index.html/app/src/examples/06-ssr/index.html
<!doctype html>
<html lang="en">
<head>
  <meta charset="utf-8">
  <meta name="viewport" content="width=device-width, initial-scale=1">
  <title>06 · Streaming SSR</title>
  <link rel="stylesheet" href="/public/styles.css">
</head>
<body>
<main>
  <p class="crumbs"><a href="/">&larr; back to index</a></p>
  <h1>06. streaming SSR with DPU</h1>
  <p class="lede">The entire page is server-rendered in one HTTP response. The shell flushes immediately, then each section streams in as its backend resolves. Out-of-order. Zero client JS. Refresh as many times as you want.</p>

  <section class="demo-area">
    <?start name="user">
      <div class="skeleton lg"></div>
      <div class="skeleton sm"></div>
    <?end>
  </section>

  <h3>feed</h3>
  <section class="demo-area">
    <?start name="feed">
      <div class="skeleton"></div>
      <div class="skeleton"></div>
      <div class="skeleton sm"></div>
    <?end>
  </section>

  <h3>shop</h3>
  <section class="demo-area">
    <?start name="shop">
      <div class="skeleton lg"></div>
      <div class="skeleton sm"></div>
    <?end>
  </section>

  <section style="margin-top:2rem;">
    <?start name="footer"><span style="color:var(--muted);font-size:.75rem;">waiting for footer...</span><?end>
  </section>

  <section class="refs">
    <h3>learn more</h3>
    <ul>
      <li><a href="https://developer.chrome.com/blog/declarative-partial-updates?hl=en" target="_blank" rel="noopener">Chrome blog: Declarative Partial Updates</a> <span class="note">— Streaming SSR is the headline use case.</span></li>
      <li><a href="https://github.com/WICG/declarative-partial-updates/blob/main/patching-explainer.md" target="_blank" rel="noopener">WICG explainer: patching</a> <span class="note">— Why <code>&lt;?start&gt;/&lt;?end&gt;</code> + <code>&lt;template for&gt;</code> can land out-of-order.</span></li>
      <li><a href="https://github.com/whatwg/html/pull/11818" target="_blank" rel="noopener">WHATWG HTML PR #11818</a> <span class="note">— The spec change for out-of-order streaming.</span></li>
      <li><a href="https://github.com/whatwg/html/issues/2142" target="_blank" rel="noopener">WHATWG HTML issue #2142</a> <span class="note">— The original 2017 thread that led here.</span></li>
      <li><a href="https://github.com/GoogleChromeLabs/web-perf-demos/blob/main/patching-demos/photo-album-server.js" target="_blank" rel="noopener">photo-album-server.js demo</a> <span class="note">— A more realistic SSR example, by the team behind the API.</span></li>
    </ul>
  </section>

  <!-- source-viewer goes here -->

  <footer class="byline">made by <a href="https://paul.kinlan.me/" target="_blank" rel="noopener">Paul Kinlan</a></footer>
</main>

open on GitHub →

lib/streaming.ts/app/src/lib/streaming.ts
export class StreamAborted extends Error {
  constructor() {
    super("stream aborted by client");
    this.name = "StreamAborted";
  }
}

export type ChunkSource = (write: (chunk: string) => Promise<void>) => Promise<void>;

const encoder = new TextEncoder();

export function streamingResponse(
  source: ChunkSource,
  contentType = "text/html; charset=utf-8",
): Response {
  let aborted = false;
  const body = new ReadableStream<Uint8Array>({
    async start(controller) {
      const write = async (chunk: string) => {
        if (aborted) throw new StreamAborted();
        try {
          controller.enqueue(encoder.encode(chunk));
        } catch {
          aborted = true;
          throw new StreamAborted();
        }
      };
      try {
        await source(write);
      } catch (err) {
        if (!(err instanceof StreamAborted)) console.error("stream source error", err);
      } finally {
        if (!aborted) {
          try {
            controller.close();
          } catch {
            // already closed
          }
        }
      }
    },
    cancel() {
      aborted = true;
    },
  });
  return new Response(body, {
    headers: {
      "content-type": contentType,
      "cache-control": "no-store",
      "x-content-type-options": "nosniff",
    },
  });
}

export function sleep(ms: number): Promise<void> {
  return new Promise((resolve) => setTimeout(resolve, ms));
}

open on GitHub →

lib/files.ts/app/src/lib/files.ts
// File helpers for examples: read sibling files, render the page shell with the
// source viewer injected, and serve sibling static assets (CSS/JS/HTML) on demand.

import { sourceBlock } from "./source.ts";

const MIME: Record<string, string> = {
  css: "text/css; charset=utf-8",
  js: "application/javascript; charset=utf-8",
  html: "text/html; charset=utf-8",
  svg: "image/svg+xml",
  json: "application/json; charset=utf-8",
};

function dirFor(handlerUrl: string): string {
  return new URL(".", handlerUrl).pathname;
}

export function readSibling(handlerUrl: string, name: string): string {
  return Deno.readTextFileSync(dirFor(handlerUrl) + name);
}

// Reads a sibling HTML file. The marker `<!-- source-viewer goes here -->` is replaced
// with the source-viewer block. Extra named placeholders ({{name}}) can be supplied via `vars`.
const SOURCE_MARKER = "<!-- source-viewer goes here -->";

export function loadShell(
  handlerUrl: string,
  name = "index.html",
  vars: Record<string, string> = {},
): string {
  let raw = readSibling(handlerUrl, name);
  raw = raw.replace(SOURCE_MARKER, sourceBlock(handlerUrl));
  for (const [k, v] of Object.entries(vars)) {
    raw = raw.replaceAll(`{{${k}}}`, v);
  }
  return raw;
}

// Try to serve a sibling static file from the example folder. Returns null when the
// file is missing, the path escapes the folder, or the path is empty.
export function tryStatic(handlerUrl: string, requestPath: string): Response | null {
  const safe = requestPath.replace(/^\/+/, "");
  if (!safe || safe.includes("..")) return null;
  // Never let the browser fetch handler.ts or README.md through this fallback.
  if (safe === "handler.ts" || safe === "README.md") return null;
  try {
    const content = Deno.readFileSync(dirFor(handlerUrl) + safe);
    const ext = safe.split(".").pop() ?? "";
    return new Response(content, {
      headers: {
        "content-type": MIME[ext] ?? "application/octet-stream",
        "cache-control": "no-store",
      },
    });
  } catch {
    return null;
  }
}

open on GitHub →