Streaming Server‑Side Rendering in React: Concepts, lazy, Suspense, and Implementation

This article explains the principles of streaming server‑side rendering (SSR) in React, compares it with traditional client‑side rendering, and demonstrates how lazy loading and Suspense can be used together with streaming SSR to parallelize data and JavaScript delivery for faster first‑paint and improved user experience.

ByteFE
ByteFE
ByteFE
Streaming Server‑Side Rendering in React: Concepts, lazy, Suspense, and Implementation

With the rapid evolution of web technologies, front‑end code has become increasingly complex, leading to larger client payloads and longer first‑paint times; server‑side rendering (SSR) is introduced as a technique to reduce initial rendering latency and improve user experience.

Traditional client‑side rendering (CSR) follows a multi‑step chain: request HTML, request JavaScript, request data, then execute JavaScript, which can delay the first paint. An illustration of this flow is shown below.

SSR reduces the first‑paint (FP) time by sending rendered HTML from the server, but it can introduce a period of non‑interactive time between FP and Time To Interactive (TTI), sometimes leaving users confused by visible content that cannot be interacted with.

Both CSR and SSR start by returning HTML, but CSR then sends JavaScript followed by data, while SSR can send data first and JavaScript later, allowing the page to become interactive sooner after the initial paint.

To further improve performance, streaming SSR is proposed. The ideal streaming SSR flow (illustrated below) uses only two requests: one for HTML that streams a skeleton screen and later the actual data, and another for JavaScript that becomes interactive once executed.

Advantages include parallel data and JavaScript fetching and a reduced Time To First Byte (TTFB) when the two requests are combined.

To enable streaming SSR, existing SSR frameworks need to be upgraded with lazy and Suspense. A minimal lazy implementation is shown below:

function lazy(loader) {
  let p;
  let Comp;
  let err;
  return function Lazy(props) {
    if (!p) {
      p = loader();
      p.then(exports => (Comp = exports.default || exports), e => (err = e));
    }
    if (err) throw err;
    if (!Comp) throw p;
    return <Comp {...props} />;
  };
}

The use of throw allows the promise to bubble up to the nearest catch, enabling the framework to pause rendering until the component is loaded. lazy is typically paired with Suspense. A simple Suspense implementation is:

function Suspense({ children, fallback }) {
  const forceUpdate = useForceUpdate();
  const addedRef = useRef(false);
  try {
    return children;
  } catch (e) {
    if (e instanceof Promise) {
      if (!addedRef.current) {
        e.then(forceUpdate);
        addedRef.current = true;
      }
      return fallback;
    } else {
      throw e;
    }
  }
}

The logic attempts to render children; if a Promise is thrown, the fallback UI is shown until the promise resolves, after which the component re‑renders.

Other frameworks such as Vue or Preact use similar placeholder mechanisms, but the core idea remains the same: a marker (often a Symbol) indicates a suspended component.

The final piece integrates streaming SSR with an Express server using ReactServerDOM.renderToNodeStream:

app.get("/", (req, res) => {
  res.write("<!DOCTYPE html><html><head><title>My Page</title></head><body>");
  res.write("<div id='root'>");
  const stream = ReactServerDom.renderToNodeStream(<App />);
  stream.pipe(res, { end: false });
  stream.on('end', () => {
    res.write("</div></body></html>");
    res.end();
  });
});

During streaming, the server first sends a skeleton HTML fragment, then later replaces it with the actual content once the associated Promise resolves, using an inline script to swap DOM nodes.

Overall, streaming SSR can significantly reduce render time and improve perceived performance, though it is still largely experimental and under active development in major frameworks.

References:

https://github.com/facebook/react/pull/20970

https://github.com/facebook/react/pull/14717

https://hackernoon.com/whats-new-with-server-side-rendering-in-react-16-9b0d78585d67

https://reactjs.org/docs/react-dom-server.html#rendertonodestream

https://zhuanlan.zhihu.com/p/56587500

https://zhuanlan.zhihu.com/p/358624196

https://github.com/overlookmotel/react-async-ssr

Original Source

Signed-in readers can open the original source through BestHub's protected redirect.

Sign in to view source
Republication Notice

This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactadmin@besthub.devand we will review it promptly.

frontendReactStreamingSSRSuspense@Lazy
ByteFE
Written by

ByteFE

Cutting‑edge tech, article sharing, and practical insights from the ByteDance frontend team.

0 followers
Reader feedback

How this landed with the community

Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.