4

I am currently creating a website and have been looking to start using renderToNodeStream to increase the performance of the server side rendering instead of using renderToString.

At the moment I am using renderToString and then using Helmet.renderStatic to get all the required meta data and title from each of the pages. When I switch to using renderToNodeStream however, I will be writing to the head prior to rendering anything, and so can no longer use Helmet.renderStatic anymore.

I was thinking I could do the below to solve this issue, but this involves first using renderToString before then using renderToNodeStream, and probably doesn't really provide much improvement...

app.use('*', (req, res) {
  Loadable.preloadAll().then(() => {
    const store = createStore(
      reducers,
      getDefaultStateFromProps(),
      applyMiddleware(thunk)
    );
    const routeContext = {};
    const router = (
      <Provider store={store}>
        <StaticRouter location={req.url} context={routeContext}>
          <App/>
        </StaticRouter>
      </Provider>
    );

    res.setHeader('Content-Type', 'text/html');

    renderToString(router);
    const helmet = Helmet.renderStatic();

    res.locals.title = helmet.title;
    res.locals.meta = helmet.meta;
    res.locals.link = helmet.link;

    res.write(headTemplate(res.locals));

    const stream = renderToNodeStream(router);

    stream.pipe(res, { end: false });
    stream.on('end', () => {
      res.locals.context = JSON.stringify(store.getState());
      res.end(bodyTemplate(res.locals));
    });
  });
}

Does anyone know how to get around this issue?

Phil
  • 1,610
  • 4
  • 26
  • 40
  • Hey Phil, did you manage to solve the issue you were facing with this? We are just about to look at using `renderToNodeStream` – Alex Turner Dec 01 '20 at 11:50
  • here is [my post](https://stackoverflow.com/questions/66050007/react-ssr-blinks-when-starting-client) for more info and help – zahra shahrouzi Feb 04 '21 at 19:15

1 Answers1

8

This has been an issue I have also been struggling with lately -- I've been trying to migrate an app from renderToString to renderToNodeStream and was having a heck of a time trying to get dynamic head data to work.

So, unfortunately react-helmet doesn't provide out of the box support for renderToNodeStream. There are two libraries that I know of that you can use. Check out:

  • react-helmet-async*
  • react-safety-helmet

*Though the documentation for react-helmet-async has a quick guide on how to use the library with renderToNodeStream, the author recently said it's not officially supported (@see https://github.com/staylor/react-helmet-async/issues/37#issuecomment-573361267)

Additionally, I see the Loadable.preloadAll function call - you're going to also have to migrate over to loadable-components which supports renderToNodeStream.

So, assuming you migrate over to loadable-components and one of the helmet libraries above, if your Head / helmet data is static, I believe everything should work for you right out of the box. If you have head data that depends on API calls, you might want to look into adding something like react-ssr-prepass.

I personally ended up using react-safety-helmet; here's the basic approach I took:

client

import { loadableReady } from '@loadable/component';
import { createHelmetStore, HelmetProvider } from 'react-safety-helmet';

const helmetStore = createHelmetStore();

loadableReady(() => {
  const root = document.getElementById('app-root')
  hydrate(
    <HelmetProvider store={helmetStore}
        <Provider store={store}>
            <StaticRouter location={req.url} context={routeContext}>
                <App/>
            </StaticRouter>
        </Provider>
    </HelmetProvider>, root)
})

server

import { renderToNodeStream } from 'react-dom/server';
import { ChunkExtractor } from '@loadable/server';
import { createHelmetStore, HelmetProvider } from 'react-safety-helmet';

const statsFile = path.resolve('../dist/loadable-stats.json')

const extractor = new ChunkExtractor({ statsFile })

new Promise((resolve, reject) => {
    const helmetStore = createHelmetStore();
    let body = '';
    const router = (
      <HelmetProvider store={helmetStore}>
            <Provider store={store}>
                <StaticRouter location={req.url} context={routeContext}>
                    <App/>
                </StaticRouter>
            </Provider>
        </HelmetProvider>,
    );
    renderToNodeStream(router)
      .on('data', (chunk) => {
        body += chunk;
      })
      .on('error', (err) => {
        reject(err);
      })
      .on('end', () => {
        resolve({
          body,
          helmet: helmetStore.renderStatic(),
        });
      });
}).then(({body, helmet}) => {
    // Create html with body and helmet object
    const linkTags = extractor.getLinkTags();
    const styleTags = extractor.getStyleTags();

    // This will be dependent on your implementation
    const htmlStates = {
        helmet, // From the resolved promise above
        store: store.getState(),
        linksTags,
        styleTags
    };

    const [startHtml, endHtml] = htmlTemplate(htmlStates); // Will vary on your implementation

    res.write(startHtml);
    res.write(body) // From the resolved promise above
    res.end(`${extractor.getScriptTags()}${endHtml}`) // This will vary as well - just make sure to add your JS tags before the closing </body></html>
});

Hopefully this helps get you on the right path. Best of luck.

  • hi! for ssr in react i'm using `react-helmet-async` , `loadable-component` , `react-router-dom` , `material-ui` so there are some sheet collectors and ofcourse `express.js`. i tried your code and it works but i still get the error of initial response time in page speed insights and my score got worse .i donno how to make it so i could get the first response faster .in other words i donno how to get my response in stream manner and get little bytes continuously in order to increase my performance .thank u in advance @Jon Eric Escobedo – zahra shahrouzi Feb 04 '21 at 08:43
  • when i saw the decrease in performance i moved back to using renderToStaticMarkup .but as i said there is high loss of score on initial response time .the other matter is that i'm getting a re-render when it finishes ssr and starts csr .so it blinks and creates a not so awesome user experience . i'd be glad to here any solution i could try. – zahra shahrouzi Feb 04 '21 at 08:47
  • here is [my post](https://stackoverflow.com/questions/66050007/react-ssr-blinks-when-starting-client) for more info and help – zahra shahrouzi Feb 04 '21 at 19:14