React Code Splitting in 2019
It’s 2019! Everybody thinks they know code splitting. So — let’s double check!

What does code splitting stand for?
In short, code splitting is just about not loading a whole element, but loading only part of it. When you are reading this page, you don’t have to load the whole site. When you are selecting a single row from a database – you don’t have to take all the data. Obvious? Code splitting is also quite obvious, it is not just about your data, but your code.
Who is making code splitting?
React.lazy?
No – it’s just using it. Code splitting is done on a bundler level – webpack, parcel, or just your file system in the case of “native” esm modules. Code splitting is just files, files you can load somewhere “later”.
Who is using code splitting?
React.lazy
is using it. Just using code splitting of your bundler. Just calling import
when got rendered. And that’s all.
What’s about React-loadable?
React.lazy
superseded it. And provided more features, like Suspense
to control loading state. So — use React.Lazy
instead.
Yep, that’s all. Thank you for reading and have a nice day.
Why isn’t the article finished?
Well… There are a few grey zones about React.lazy and code splitting I forgot to mention.
Grey Zone 1 – testing
It’s not easy to test `React.lazy` due to its asynchronous nature. The result of mount(MyLazyComponent)
would be just “empty”, as long as the “real” Component
, behind MyLazy
, is not loaded yet. And even if it is – import
returns, and lazy
accepts, promises, which always got executed in the next tick. So — you will never get Component in the current tick. It’s the law!
const LazyComponent = lazy(() => import('/path/to/dynamic/component'));
const Fallback = () => <div />;
const SuspenseComponent = () => (
<Suspense fallback={<Fallback />}>
<LazyComponent />
</Suspense>
);const wrapper = mount(<SuspenseComponent />)
expect(wrapper.find('Fallback')).to.have.lengthOf(1)
expect(wrapper.find('DynamicComponent')).to.have.lengthOf(0)
// ^ not loadedawait wrapper.waitUntilLazyLoaded()expect(wrapper.find('Fallback')).to.have.lengthOf(0)
expect(wrapper.find('DynamicComponent')).to.have.lengthOf(1)
// ^ loaded!
Proposed solution? You would not believe, but the proposed solution is to use synchronous thenables.
const LazyText = lazy(() => ({
then(cb) {
cb({default: Text});
// this is "sync" thenable
},
})); const root = ReactTestRenderer.create(
<Suspense fallback={<Text text="Loading..." />}>
<LazyText text="Hi" /> // this lazy is not very lazy
</Suspense>,
);
It’s not hard to convert import function to a memorized synchronous thenable.
const syncImport = (importFn) => {
let preloaded = undefined;
const promise = importFn().then(module => preloaded = module);
// ^ "auto" import and "cache" promise return () => preloaded ? { then: () => preloaded } : promise;
// ^ return sync thenable then possible
}const lazyImport = isNode ? syncImport : a => a;
// ^ sync for node, async for browserconst LazyComponent = React.lazy(lazyImport(() => import('./file'));
Grey zone 2 – SSR
If you DON’T need SSR – please continue reading the article!
React.lazy
is SSR friendly. But it requires Suspense
to work, and Suspense
is NOT server side friendly.
There are 2 solutions:
- Replace
Suspense
byFragment
, just mock it out. Then - use altered version syncthenable
to make lazy also sync.
import React from 'react';const realLazy = React.lazy;
React.lazy = importer => realLazy(syncImport(importer));
React.Suspense = React.Fragment; // :P// ^ React SSR just got fixed.
This is a good option, but it wouldn’t be quite client side friendly. Why? Let’s define the 2nd possible solution:
- Use a specialised library to track used scripts, chunks and styles, and load them on the client side (especially styles!) before react hydration. Or else – you would render empty holes instead of your code splitted components. Yet again – you didn’t load the code you just splitted, so you can’t render anything you are going to.
Behold code splitting libraries
- Universal-component – the oldest, and still maintainable library. It “invented” code splitting in terms of – taught Webpack to code split.
- React-loadable – very popular, but unmaintained library. Made code spitting a popular thing. Issues are closed, so there is no community around.
- Loadable-components – a feature complete library, it’s a pleasure to use, with the most active community around.
- Imported-component – a single library, not bound to Webpack, ie capable to handle parcel or esm.
- React-async-component – already dead library(yet popular), which made a significant impact on everything around code splitting, custom React tree traversal and SSR.
- Another library – there were many libraries, many of which did not survive Webpack evolution or React 16 – I havent listed them here, but if you know a good candidate – just DM me.
Which library to pick?
It’s easy – not react-loadable – it’s heavy unmaintained and obsolete, even if it is still mega popular. (and thank you for popularizing code splitting, yet again)
- Loadable-components – might be a very good choice. It is well written, actively maintained and supports everything out of the box. Support “full dynamic imports”, allowing you to import files depending on the props given, but thus untypable. Supports Suspense, so could replace
React.lazy
. - Universal-component – actually “inventors” of full dynamic imports – as long as they implemented it in Webpack. And many other things at low level, like css-chunks, report-chunks and other -chunks – they did it. And this library authors also members of a webpack team. I would say – this library is a bit hardcore, and a bit less user friendly. Loadable-components documentation is unbeatable. It’s worth if not to use this library, then to read documentation — there are so many details you should know…
- React-imported-component – is a bit odd. It’s bundler independent, so it would never break (there is nothing to break), would work with Webpack 5 and 55, but that comes with a cost. Does not support full dynamic imports, like
React.lazy
, and, as a result – typeable. Also supports Suspense. Usessynchronous thenables
on SSR. It also has absolutely different approach for CSS, and perfect stream rendering support.
Probably it’s better to explain “the cost” of this library — in short — it may defer TTI (Time To Interactive).
“Normal” libraries during SSR would add all the used scripts to the page body, and you will be able to load all the scripts in a parallel, and “hydrate” your application once they all be ready.
TTI would be
max(mainTime, chunk1Time, chunk2Time)
imported
is “bunder-independent”, and don’t know files names to load. So it will first wait to the main bundle to load, and then, from inside the main bundle — call the original “imports
”, letting bundler
to load chunks, as it would do in case of simple React.lazy
. That’s quite solid solution, but, you have to load the main bundler first.
TTI would be
mainTime + max(chunk1Time, chunk2Time)
And, let’s be honest, ^that^ might be just two times slower.
Except some implementation details — there is no difference in quality or popularity between listed libraries, and we are all good friends – so pick the one by your heart.
Grey zone 3 – hybrid render
SSR is a good thing, but you know it’s hard. Small projects might want to have a SSR – there are a lot of reasons to have it – but don’t want to set up and maintain it.
SSR could be really, REALLY hard. Try razzle or go with Next.js if you want a quick win.
So the easiest solution for SSR, especially for simple SPA would be prerendering. Like opening your SPA in a browser and hitting the “Save” button. Like:
- React-snap — uses puppeteer to render your page in a “browser” and saves a result
- Rendertron — which does the same, but in a different (in google clouds) way.
Prerendering is “SSR” without “Server”. It’s SSR using a Client. Magic! And working out of the box… … … but not for code spitting.
So — you just rendered your page in a browser, saved HTML, and asked to load the same stuff. But Server Side Specific Code (to collect all used chunks) was not used, cos THERE IS NO SERVER!

In the previous part, I pointed to libraries which are bound to webpack in terms of collecting information about used chunks — they could not handle hybrid render at all.
Loadable-components version 2 (incompatible with current version 5), was partially supported by react-snap. Support has gone.
React-imported-component
could handle this case, as long as it is not bound to the bundler/side. So there is no difference — is it SSR or Hybrid — it would work even in hybrid mode, but only for the hybrid modereact-snap
could provide, as long as it provides some hooks for “state hydration”, while rendertron
just renders everything.
This ability of
react-imported-componen
to work with react-snap, was found while writing this article, it was not known before — see example. It’s quite easy.
And here you have to use another solution, which is just perpendicular to all other libraries.
React-prerendered-component
This library was created for partial hydration, and could partially rehydrate your app, keeping the rest still de-hydrated. And it works for SSR and Hybrid renderers without any difference.
The idea is simple:
- during SSR — render the component, wrapped with a <div/>
- on the client — find that div, and use
innerHTML
untilComponent
is ready to replace dead HTML. - you don’t have to load, and wait for a chunk with splitted component to load to
"NOT render a white hole instead of it"
— use pre-rendered HTML. The code you got from a server already contain all the HTML you have to display. That’s why we have to wait for all the chunks to load beforehydrate
— to match server-rendered HTML. That’s why we could use pieces of server-rendered HTML until client is not ready.
That’s why we have to wait for all the chunks to load before hydrate — to match server-rendered HTML. That’s why we could use pieces of server-rendered HTML until client is not ready — it is equal to the one we are only going to produce.
import {PrerenderedComponent} from 'react-prerendered-component';const importer = memoizeOne(() => import('./Component'));
// ^ it's very important to keep the "one" promiseconst Component = React.lazy(importer);
// or use any other library with ".prefetch" support
// all libraries has it (more or less)const App = () => (
<PrerenderedComponent live={importer()}>
{/* ^ shall return the same promise */ }
<Component />
{/* ^ would be rendered when component goes "live" */ }
</PrerenderedComponent>
);
There is also another article about it, written before, and without hybrid rendering in mind:
TLDR?
- don’t use react-loadable, it would not add any valuable value
- React.lazy is good, but too simple, yet.
- SSR is a hard thing, and you should know it
- Hybrid puppeteer-driven rendering is a thing. Sometimes even harder thing.
Please send kudos to:
Bergé Greg — loadable components author
Sean Matheson — react-async-component (even if they are a bit dead)
James Gillmore and Zack Jackson — universal-component creators
Anton Korzunov — for react-imported and prerendered-components
And all the others who are inventing new ways and principles, making our live sometime easier, and sometimes harder.
Still happy about React.lazy?
It’s cool, but we have to go deeper.