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 thing. Then you are reading this page you don't have to load a whole site. When you are selecting a single row from a database – you don't have to take all.
Obvious? Code splitting is also quite obvious, just not about your data, but your code.
Who is making code-splitting?
React.lazy
? No – it only uses it. Code-splitting is working on a bundler level – webpack, parcel, or just your file system in 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. 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 have 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 this article is not 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 asynchroniosity. It would be just "empty", as long as it is not loaded yet(even if it is) – Promises
and import
returns, and lazy accepts, promises, which always got executed in the next tick.
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 loaded
await wrapper.waitUntilLazyLoaded()
// ^ Oh god, why!?
expect(wrapper.find('Fallback')).to.have.lengthOf(0)
expect(wrapper.find('DynamicComponent')).to.have.lengthOf(1)
// ^ loaded!
Proposed solution? You would not believe it, but the proposed solution is to use synchronous thenables.
Support sync thenables for lazy() #14626
Why don't we? Currently they fail with a confusing error because of a race condition (status gets set to resolved but then the result gets overwritten by the next line). This should fix it.
I figured this might be useful for testing. See https://github.com/airbnb/enzyme/issues/1917#issuecomment-454208642. It's awkward that people look for workarounds like waitForLazyLoaded
in a synchronous environment. Supporting sync thenables could be a nice solution to that.
So - let's make out import SYNCHRONOUS!!
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 memoized 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 browser
const 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 with Fragment, via mocking for example. Then, use the altered version of
import
with synchronousthen
to make lazy also behave synchronously.
import React from 'react';
const realLazy = React.lazy;
React.lazy = importer => realLazy(syncImport(importer));
React.Suspense = React.Fragment; // :P
// ^ React SSR just got fixed :D
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 client-side (especially styles!) before React hydration. Or else – you would render empty holes instead of your code split components. Yet again – you didn't load the code you just split, 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 haven't 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 very well written, actively maintained and support 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 – they implemented it in Webpack. And many other things at low-level – they did it. 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 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. While previous 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 – imported don't know files names, and will call the original "imports"(that's why bundle independent) to load used chunks, but able to make calls only from inside the main bundle – so all additional scripts would be loaded only after the main one got downloaded and executed. Does not support full dynamic imports, like React.lazy, and, as a result – typeable. Also supports Suspense. Uses synchronous thenables on SSR. It also has an absolutely different approach for CSS, and perfect stream rendering support.
There is no difference in quality or popularity between listed libraries, and we are all good friends – so pick by your heart.
gregberge / loadable-components
The recommended Code Splitting library for React ✂️✨
React code splitting made easy. Reduce your bundle size without stress ✂️✨.
npm install @loadable/component
See the documentation at loadable-components.com for more information about using Loadable Components!
Quicklinks to some of the most-visited pages:
Example
import loadable from '@loadable/component'
const OtherComponent = loadable(() => import('./OtherComponent'))
function MyComponent() {
return (
<div>
<OtherComponent />
</div>
)
}
Supporting Loadable Components
Loadable Components is an MIT-licensed open source project. It's an independent project with ongoing development made possible thanks to the support of these awesome backers. If you'd like to join them, please consider:
License
Licensed under the MIT License, Copyright © 2017-present Greg Bergé.
See LICENSE for more information.
theKashey / react-imported-component
✂️📦Bundler-independent solution for SSR-friendly code-splitting
IMPORTED COMPONENT ✂
Code splitting which always works*
SSR-friendly code splitting compatible with any platform
Deliver a better experience within a single import.
* It's really will never let you down. All credits to your bundler.
👉 Usage | API | Setup | SSR | CCS Concurrent loading | Webpack/Parcel
Library
Suspense
SSR
Hooks
Library
Non-modules
import(
./${value}
)
babel-macro
webpack only
React.lazy
✅
❌
❌
❌
❌
❌
😹
no-ssr
react-loadable
✅
✅
❌
❌
✅
❌
❌
😿
@loadable/component
✅
✅
❌
✅
❌
✅
❌
😿
imported-component
✅
✅
✅
✅
✅
❌
✅
😸
Read more about what this table displays
Key features:
- 1️⃣ Single source of truth - your bundler drives everything
- 📖 library level code splitting
- 🧙️ Hybrid and Prerendering compatible
- 💡 TypeScript bindings
- ⚛️ React.Lazy underneath (if hot module updates are disabled)
- 🌟 Async on client, sync on server. Supports Suspense (even…
faceyspacey / react-universal-component
🚀 The final answer to a React Universal Component: simultaneous SSR + Code Splitting
React Universal Component
🍾🍾🍾 GIT CLONE 3.0 LOCAL DEMO 🚀🚀🚀
Intro
For "power users" the traditional SPA is dead. If you're not universally rendering on the server, you're at risk of choking search engine visibility. As it stands, SEO and client-side rendering are not a match for SSR. Even though many search engines claim better SPA indexing, there are many caveats. Server-side rendering matters: JavaScript & SEO Backfire – A Hulu.com Case Study
The real problem has been simultaneous SSR + Splitting. If you've ever attempted such, you know. Here is a one-of-a-kind solution that brings it all together.
This is the…
Grey zone 3 – hybrid render
SSR is a good thing, but, you know, hard. Small projects might want to have a SSR – there are a lot of reasons to have it –but they might not want to setup and maintain it.
SSR could be really, REALLY hard. Try
razzle
or go withNext.js
if you want a quick win.
So the easiest my solution for SSR, especially for simple SPA would be prerendering. Like opening your SPA in a browser and hitting "Save" button. Like:
- React-snap - uses puppeteer(aka headless Chrome) to render your page in a "browser" and saves a result as a static HTML page.
- Rendertron - which does the same, but in a different (cloud) 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've 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 for SSR or Hybrid, but only for react-snap
, as long as it supports "state hydration", while rendertron
does not.
This ability of react-imported-component 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
- on the client - find that div, and use innerHTML until Component is ready to replace dead HTML.
- you don't have to load, and wait for a chunk with split component to NOT render a white hole instead of it - just use pre-rendered HTML, which is absolutely equal to the one a real component would render, and which already exists - it comes with a server(or hybrid) response .
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 the 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" promise
const 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>
);
theKashey / react-prerendered-component
🤔Partial hydration and caching in a pre-suspense era
Idea
In short: dont try to run js code, and produce a react tree matching pre-rendered one, but use pre-rendered html until js code will be ready to replace it. Make it live.
What else could be done on HTML level? Caching, templatization, and other good things to 🚀, just in a 3kb*.
Prerendered component
Render something on server, and use it as HTML on the client
- Server side render data
- call
thisIsServer
somewhere, to setup enviroment. - React-prerendered-component
will leave trails
, wrapping each block with div with known id.
- call
- Hydrate the client side
- React-prerendered-component will search for known ids, and
read rendered HTML
back from a page.
- React-prerendered-component will search for known ids, and
- You site is ready
- React-prerendered-components are ready. They are rendering a pre-existing HTML you send from a server.
- Once any component ready to be replaced - hydrate
- But not before. That's the point…
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.
Top comments (0)