javascriptnode.jsreactjsserver-side-renderingreact-dom-server

Why does `React.renderToNodeStream` not yield to the event loop?


I'm trying to make React's renderToString and renderToStaticMarkup a better citizen by yielding to the event loop allowing other server requests to get a look in, e.g.

const React = require('react');
const { renderToNodeStream } = require('react-dom/server');

// Wrap `renderToNodeStream` in promise
const renderToStringAsync = node => {
  return new Promise((resolve, reject) => {
    let body = '';
    const stream = renderToNodeStream(node);
    // NOTE: we're turning the tap on full blast here, but I still expected it to yield
    stream.on('data', chunk => {
      console.log('Received chunk');
      body += chunk.toString();
    });
    stream.on('error', ex => {
      reject(ex);
    });
    stream.on('end', () => {
      resolve(body);
    });
  });
};

setTimeout(() => {
  console.log('Yielded to event loop');
}, 0)
await renderToStringAsync(largeRootNode);

I expected this:

// Expect:
// Received chunk
// Yielded to event loop
// Received chunk
// Received chunk
// Received chunk
// Received chunk
// Received chunk
// Received chunk
// Received chunk
// Received chunk
// Received chunk

But I actually get this:

// Actual:
// Received chunk
// Received chunk
// Received chunk
// Received chunk
// Received chunk
// Received chunk
// Received chunk
// Received chunk
// Received chunk
// Yielded to event loop

I'm wondering if it's related to .on('data'); I know it doesn't manage backpressure, but I had always thought it would be async?

NOTE: I'm not piping the response to the client as I need to wait for the render to complete before determining the status code; I merely want to use renderToNodeStream to improve cooperative multitasking in node.js)


Solution

  • Promise executors are executed synchronously and so is ReactDOMNodeStreamRenderer. The _read method of ReactDOMNodeStreamRender has no asynchronous components and will be called synchronously as per the method contract in Node.

    In short, the entire code block here is executed synchronously with no synchrony involved. The streaming interface just gives the potential for this to be executed asynchronously and also makes it slightly easier to pipe to a stream which indeed writes asynchronously.

    It's important to note that the stream interface does not inherently make any operation asynchronous!