Skip to content

RFC: Complete opt-in streaming SSR with cancellation and hydration guarantees #2526

Description

@benpsnyder

Which scope/s are relevant/related to the feature request?

platform, router, Docs

Information

This is the separate streaming proposal requested by Brandon Roberts on analogjs/analog#2516. The existing experimental renderer and Angular capture transform need to be connected to the split Vite/Nitro integration and qualified as one opt-in path.

The use case is a dashboard whose shell and fast panel can render while a slower deferred panel is still pending. The browser should show progress, then hydrate against the complete authoritative document without refetching the deferred server data.

Proposed configuration:

// vite.config.ts
plugins: [
  analog({ experimental: { streaming: true } }),
  angular(),
  nitro({
    routeRules: {
      '/checkout': { streaming: false },
    },
  }),
];
// main.server.ts
import '@angular/platform-server/init';
import { renderStream } from '@analogjs/router/server';
import { App } from './app/app';
import { config } from './app/app.config.server';

export default renderStream(App, config);

The intended contract is:

  • Explicit experimental opt-in; the Angular capture transform runs only for supported SSR builds. The proposed minimum is Angular 21, with actual qualified versions recorded in the PR. Ordinary buffered rendering and browser builds retain their existing behavior.
  • Send the shell, then resolved deferred blocks, then the authoritative hydration document. Escaped previews must not alter the final document or create client refetches.
  • Keep capture context isolated across overlapping native-async and Zone-based renders. Dispose the Angular platform and pending work on completion, failure, cancellation, or a disconnected response body.
  • Keep prerendering, crawler responses, and routes opting out of streaming fully buffered. Prerender checks must inspect useful complete HTML, not merely a successful build exit.
  • Before headers are sent, preserve the normal SSR error status. After the shell is sent, emit a generic failure marker and close safely. The browser treats missing completion data or unexpected EOF as an incomplete document, with no exception details exposed.
  • Prevent compression from buffering progressive delivery; preserve ordinary encoding behavior for buffered responses and static assets.
  • Pass optional signal, streaming, waitUntil, and renderErrorsAsHtml fields through ServerContext so HTTP hosts can retain cleanup through disconnects. Direct renderer consumers retain stream-error behavior unless they opt into HTML failure framing.

The contribution will include runnable Node, Bun, and local Workerd fixtures and will state the exact tested runtime/version boundaries. Worker qualification requires the relevant Node compatibility support for Angular/async context and a host lifetime hook. Native server-function dispatch still requires Node request/response objects; Worker streaming does not imply that native server functions are portable to Workers.

This proposal covers streaming SSR only. It does not add server components or reopen the broader, closed analogjs/analog#1574.

The other workstreams are published separately in analogjs/analog#2520 (compiler), analogjs/analog#2527 (Vitest), analogjs/analog#2523 and analogjs/analog#2528 (SSR), analogjs/analog#2529 (HTTP), and analogjs/analog#2530 (request/hydration). analogjs/analog#2531 contains the focused streaming implementation and documents the shared prerequisites required for independent green builds.

The implementation and public qualification fixture are published in analogjs/analog#2531, targeting alpha, at dcf5dcf91a93844e37ee5849e2781116396da9f0. Current-head CI passes both Angular 21.0.0/Vite 7.0.0/TypeScript 5.9.3 and Angular 22.0.0/Vite 8.0.8/TypeScript 6.0.2 rows, each on Node 24.15.0, Node with Zone.js, Bun 1.4.2, and local Workerd 1.20260730.1. The runtime tests cover gated early-byte delivery, concurrent captures, buffered/prerender output, browser hydration, error/EOF handling, real-socket disconnect cleanup, and server/asset separation. All three built deferred server-function/hydration example tests pass, including requests carrying caller SSR/streaming hints. Packed-artifact hashes and consumer locks accompany qualification results. Streaming remains experimental and opt-in; this RFC stays open for maintainer review and merge.

Describe any alternatives/workarounds you're currently using

Buffered render() remains available and is appropriate when the final load status must be known before committing HTTP headers. Client-side loading can show placeholders, but does not provide server-rendered deferred data and its hydration state. The original combined integration branch remains preserved as the historical reference; the focused implementation and public fixtures are available in the linked streaming PR.

I would be willing to submit a PR to fix this issue

  • Yes
  • No

Activity

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

Metadata

Metadata

Assignees

No one assigned

    Labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions