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).
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
}
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 toimport()
. -
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)