Skip to content

SYSTEM Cited by 1 source

Confluence Streaming SSR

Atlassian Confluence's server-side rendering tier, rebuilt in 2025–26 on top of React 18's renderToPipeableStream to emit HTML progressively at <Suspense> boundaries instead of holding the response until the whole tree rendered. A NodeJS transform pipeline sequences per-chunk state injection before markup, force-flushes compression, and signals intermediate nginx proxies not to buffer — so the chunks actually reach the browser in real time. Delivered ~40% First Contentful Paint improvement as one of several changes that halved Confluence's p90 page-load time over 12 months.

Pipeline (server)

  1. renderToPipeableStream(<App />) produces a stream of HTML chunks as each ready <Suspense> boundary resolves.
  2. State-injection transform (object mode): listens on getServerStream('data-stream') data emissions, buffers them while React is mid-chunk, flushes them before the corresponding markup on each setImmediate tick. (Ordering matters — hydration fails if state arrives after markup.) See concepts/react-hydration.
  3. Page-annotation transforms: start/end markers, script-preload <link> tags, metrics markers. Also object mode; search windows bounded so regex doesn't re-scan already-emitted content.
  4. compression middleware, force-flushed on setImmediate after each chunk — otherwise it holds partial compressed output.
  5. Response header X-Accel-Buffering: no so the upstream nginx proxy does not re-buffer. See concepts/head-of-line-buffering.

Pipeline (client)

  • Initial HTML contains pending boundaries as <!--$?--> + <template id="B:N">. React's inline-JS runtime replaces placeholders as chunks arrive.
  • Hydration runs per-boundary using state already injected ahead of the markup. See patterns/suspense-boundary.
  • JS bundle preload driven by asset prediction (from prior-render component IDs) and refined mid-stream by component-ID metadata pushed inline. See patterns/asset-preload-prediction.

Known pitfalls caught in production

  • Context-change hydration re-render (React 18): a ready Suspense boundary plus a context change discards and re-renders the subtree — once per boundary. Confluence mitigated with unstable_scheduleHydration to raise hydration priority; React 19 fixes it properly. A leaky state-management library compounded this with per-render listener growth → CPU regression.
  • Buffer-mode regex transforms: buffer↔string round-trip was a dominant cost on large pages; fixed by running transforms in objectMode throughout.
  • Intermediate buffering silently squashes chunks into blobs; must disable at every proxy + compression layer.

Numbers

  • ~40% improvement in First Contentful Paint from streaming SSR alone.
  • ~50% cut in time-to-interaction from the asset-preload-prediction feedback loop.
  • Rollout measured FCP, TTVC (target VC90), TTI, hydration success rate at p50/p90/p99 via A/B test over "several weeks".

Seen in

Last updated · 200 distilled / 1,178 read