DEV Community

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

Posted on

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

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

Add postbuild hook to the package.json:

"scripts": {
  "postbuild": "react-snap"
}

And you're done!

I also added

"reactSnap": {
  "inlineCss": true
}

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);

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>;
};
  • 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

Top comments (0)