Friday hack: Suspense, Concurrent mode and lazy to load locales for i18n

stereobooster - Dec 15 '18 - - Dev Community

I have a small series of posts about Lingui. I implemented all i18n related features. And I want to add prerendering to improve load performance, but it appears not that simple as it supposes to be. I had to "hack" Suspense, ConcurrentMode and React.lazy.

As I said this is a hack, this is done for fun. Do not use this code in production, unless you know what you are doing.

The full source code is here

In the previous episode

We stopped here: i18n of React with Lingui.js #3. I deployed it to Github pages and measured load performance with webpagetest (From: Dulles, VA - Moto G4 - Chrome - 3G).

filmstrip 1

As you can see it takes way to long to get first paint (4-4.5s). The easiest way to fix it, given that we use CRA and don't want to eject, it to use react-snap.

Add prerendering with the help of react-snap

npm install --save react-snap
 # or using Yarn
yarn add react-snap
Enter fullscreen mode Exit fullscreen mode

Add postbuild hook to the package.json:

"scripts": {
  "postbuild": "react-snap"
}
Enter fullscreen mode Exit fullscreen mode

And you're done!

I also added

"reactSnap": {
  "inlineCss": true
}
Enter fullscreen mode Exit fullscreen mode

filmstrip

As you can see an issue with slow first paint went away, but there is a flash of the white screen.

Flash of the white screen

On the one side, we have prerendered HTML which will start to render as soon as the browser will get it (around 2s in the US on average 3G). On the other side, we have React which will start to render as soon as all scripts will be downloaded (around 3s in the US on average 3G, for the given example).

When React will start to render and if not all dynamic resources will be loaded it will flush all the content it has and typically this is the almost white (empty) screen. This is where we get "Flash of the white screen". Dynamic resources can be: async components (React.lazy(() => import())), locale catalogs (import("./locales/" + locale + "/messages.js");).

To solve the problem we need to wait for all resources to load before React will flush the changes to the DOM.

We can do this with loader library like, react-loadable or loadable-components. See more details here.

Or we can do this with new React.lazy, <Suspense /> and <ConcurentMode />.

ConcurentMode

<ConcurentMode /> marked as unstable (use at your own risk), so it can change in the future. Read more on how to use it and about caveats here.

const ConcurrentMode = React.unstable_ConcurrentMode;
const RootApp = (
  <ConcurrentMode>
    <Suspense fallback={<div>Loading...</div>} maxDuration={5000}>
      <App />
    </Suspense>
  </ConcurrentMode>
);
const rootElement = document.getElementById("root");
const root = ReactDom.unstable_createRoot(rootElement, { hydrate: true });
root.render(RootApp);
Enter fullscreen mode Exit fullscreen mode

This is the first hack we need.

The second one is that we need to repurpose React.lazy to wait for subresource. React team will eventually add Cache for this, but for now, let's keep hacking.

const cache = {};
export default ({ locale, children }) => {
  const SuspendChildren =
    cache[locale] ||
    React.lazy(() =>
      i18n.activate(locale).then(() => ({
        __esModule: true,
        default: ({ children }) => (
          <I18nProvider i18n={i18n}>{children}</I18nProvider>
        )
      }))
    );
  cache[locale] = SuspendChildren;
  return <SuspendChildren>{children}</SuspendChildren>;
};
Enter fullscreen mode Exit fullscreen mode
  • i18n.activate(locale) returns promise, which we "convert to ES6" module e.g. i18n.activate(locale).then(() => ({ __esModule: true, ...})) is equivalent to import().
  • default: ... - default export of pseudo ES6 module
  • ({children}) => <I18nProvider i18n={i18n}>{children}</I18nProvider> react functional component
  • <SuspendChildren /> will tell <Suspense /> at the top level to pause rendering until language catalog is loaded

<ConcurentMode /> will enable <StrictMode /> and it will complain about unsafe methods in react-router, react-router-dom. So we will need to update to beta version in which issue is fixed. react-helmet also incompatible with <StrictMode />, so we need to replace it with react-helmet-async.

One way or another but we "fixed" it.

Photo by Pankaj Patel on Unsplash

. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .