Sign in
Log inSign up
Serve Content Faster with React

Serve Content Faster with React

Mayank Chandola's photo
Mayank Chandola
·Mar 14, 2016

Server Rendering

One of the advantages of React is that we can render it on the server. React provides ReactDOM.renderToString method for rendering markup on the server. ReactDOM.renderToString is synchronous so server can't send any response until the entire HTML is created. Here is some more information on the limitations of ReactDOM.renderToString.


Streaming Server Render

ReactDOM.renderToString is synchronous so our TTFB (Time To First Byte) is long. Streaming our content can decrease the TTFB.

Fortunately we have react-dom-stream by Sasha Aickin , which is a streaming server-side rendering library for React.

As react-dom-stream renders to a stream, it has a better TTFB (Time To First Byte) than ReactDOM.renderToString.

Streaming server rendered React response is fast, but we can still render faster.


Streaming Server Rendered React reponse inside Service Workers

We can create a stream inside Service Workers where the header and footer is served from the cache, and the content is streamed from the server. It is the fastest way to serve content.

I made a simple demo for it.

app.get('/stream-render', (req, res) => {
  var stream = ReactDOMStream.renderToString(<DemoComponent />);
  stream.pipe(res, {end: false});
  stream.on("end", function() {
      res.end();
  });
});

ReactDOMStream renders to a readable stream which is returned from the method. We can stream its response inside service workers.

Inside Service Workers we can intercept the network request and send the header and footer from the cache and stream the content from the server. Piping is not supported yet, so we have to combine streams manually.

function streamContent() {
  try {
    new ReadableStream({});
  }

  catch(e) {
    return new Response("Streams are not supported in your browser!");
  }
  const stream = new ReadableStream({
    start(controller) {
      const startFetch = caches.match('/shell-start.html');
      const contentFetch = fetch('/stream-render').catch(() => new Response("Error"));
      const endFetch = caches.match('/shell-end.html');

      function pushStream(stream) {
        const reader = stream.getReader();
        function read() {
          return reader.read().then(result => {
            if (result.done) return;
            controller.enqueue(result.value);
            return read();
          });
        }
        return read();
      }

      startFetch
        .then(response => pushStream(response.body))
        .then(() => contentFetch)
        .then(response => pushStream(response.body))
        .then(() => endFetch)
        .then(response => pushStream(response.body))
        .then(() => controller.close());
    }
  });

  return new Response(stream, {
    headers: {'Content-Type': 'text/html'}
  })
}

Note: Streams are only supported in Chrome Canary and you'll need chrome://flags/#enable-experimental-web-pl… enabled.

Further Reading