I thought of putting this code in a repo because of the struggles I had to face while trying to build a similar solution for a project in my company. For someone that may find themselves in a similar situation, I hope this project helps them to bootstrap React using SSR with Loadable Components in little to no time π.
Implement Code Splitting in React using Loadable Components specially when you are doing Server Side Rendering.
While the official documentation of Loadable Components try to make it sound easy to implement code splitting using their library after you follow the steps given at https://loadable-components.com/docs/server-side-rendering/, the reality could be a little different π₯. There are a lot of nuances that you'll come across once you start implementing the steps specially coming to terms with some of the keywords like node stats, web stats and which one is really needed over the other. Some of the terms are obviously not clear and one very important step(the most crucial one tbh and one which I didn't find nowhere π and had to discover after a lot of experimentation), is missing in their documentation or rather they should have done a better job of calling it out because what Loadable Components does for you is great otherwise.
- Save you from days worth of time figuring out some missing but obvious things while trying to implement Code Splitting using Loadable Components and SSR.
- Provide you with some crucial code that you can plugin AS IS in your existing app to make it work.
File Name | Description |
---|---|
src/index.tsx | Renders/Hydrates the app on the browser |
src/App.tsx | Uses React Router to load and match 2 routes; / matching to home-page.tsx and /loadable-page matching to loadable-page.tsx respectively while dynamically importing them both |
config-overrides.js | Used by customize-cra to apply your custom webpack configuration before react-script's configuration takes over. customize-cra works with react-app-rewired to run most of react scripts |
webpack-config.js | Custom webpack configuration to bundle the code before it can be used server side with Express. |
server/index.js | Entry Point for the custom webpack configuration above to start the Express Instance and serve React App Server Side. |
Configure React Scripts to use the Loadable Babel and Webpack Plugins which enables naming the chunks and enabling Code Splitting respectively
const { override, addWebpackPlugin, addBabelPlugin } = require("customize-cra");
const LoadablePlugin = require("@loadable/webpack-plugin");
module.exports = override(
addBabelPlugin("@loadable/babel-plugin"),
addWebpackPlugin(new LoadablePlugin())
);
Line 20; Configure Loadable Babel Plugin which enables naming the chunks by a logical name
plugins: ["@loadable/babel-plugin"],
Line 50; Configure Loadable Webpack Plugin so that Code Splitting can work
plugins: [new LoadablePlugin()],
When you'll run npm run build
which looks like this "build": "rimraf build && react-app-rewired build && npx webpack"
, because of the config-overrides, the react-scripts
are going to generate a file called loadable-stats.json
right under the build folder and webpack
command is going to generate a similar file but under build/server directory. As my comments in the following code also emphasize, use the file under build folder which is also referred to as the web stats during the chunk extraction process.
// This is the stats file generated by webpack loadable plugin.
// It's very important that you use the web stats instead of node stats,
// so use /build/loadable-stats.json instead of /build/server/loadable-stats.json
const statsFile = path.resolve("./build/loadable-stats.json");
// We create an extractor from the statsFile
const extractor = new ChunkExtractor({ statsFile });
// Wrap your application using "collectChunks"
const jsx = extractor.collectChunks(
<StaticRouter location={req.url}>
<App />
</StaticRouter>
);
// Render your application
const html = ReactDOMServer.renderToString(jsx);
// You can also collect your "preload/prefetch" links
const linkTags = extractor.getLinkTags(); // or extractor.getLinkElements();
// console.log(linkTags);
// And you can even collect your style tags (if you use "mini-css-extract-plugin")
const styleTags = extractor.getStyleTags(); // or extractor.getStyleElements();
// console.log(styleTags);
// You can now collect your script tags
// I removed async from script tags as it wasn't working as expected when the scripts were loaded asynchronously
const scriptTags = extractor.getScriptTags().replace(/async /g, ""); // or extractor.getScriptElements();
const indexFile = path.resolve("./build/index.html");
fs.readFile(indexFile, "utf8", (err, data) => {
if (err) {
console.error("Something went wrong:", err);
return res.status(500).send("Oops, better luck next time!");
}
return res.send(
data
.replace('<div id="root"></div>', `<div id="root">${html}</div>`)
.replace(
'<script>console.log("loadable tags placeholder")</script>',
`${linkTags}\n${styleTags}\n${scriptTags}`
)
);
});
- Run
npm run build
and thennpm run start-ssr
if you prefer to build and run the app separately. - Or, just run
npm run start-ssr-watch
which runs both of the above commands for you and also watches for any changes and rebuilds and restarts the server upon changes.
webpack cli generating the following chunks; you can see that internally it maps the numbered chunks to logical names on the right
Checkout how the chunks will be loaded on demand while SSR would still work if you reload a route. Happy Coding βοΈ