Code splitting. Code splitting is everywhere. However, why? Just because there is too much of javascript nowadays, and not all are in use at the same point in time.
JS is a very heavy thing. Not for your iPhone Xs or brand new i9 laptop, but for millions(probably billions) of slower devices owners. Or, at least, for your watches.
So — JS is bad, but what would happen if we just disable it — the problem would be gone… for some sites, and be gone "with sites" for the React-based ones. But anyway — there are sites, which could work without JS… and there is something we should learn from them...
Code splitting
Today we have two ways to go, two ways to make it better, or to not make it worse:
1. Write less code
That's the best thing you can do. While React Hooks
are letting you ship a bit less code, and solutions like Svelte
let you generate just less code than usual, that's not so easy to do.
It's not only about the code, but also about functionality — to keep code "compact" you have to keep it "compact". There is no way to keep application bundle small if it's doing so many things (and got shipped in 20 languages).
There are ways to write short and sound code, and there are ways to write the opposite implementation — the bloody enterprise. And, you know, both are legit.
But the main issue — the code itself. A simple react application could easily bypass "recommended" 250kb. And you might spend a month optimizing it and make it smaller. "Small" optimizations are well documented and quite useful — just get bundle-analyzer
with size-limit
and get back in shape.
There are many libraries, which fight for every byte, trying to keep you in your limits — preact and storeon, to name a few.
But our application is a bit beyond 200kb. It's closer to 100Mb. Removing kilobytes makes no sense. Even removing megabytes makes no sense.
After some moment it's impossible to keep your application small. It will grow bigger in time.
2. Ship less code
Alternatively, code split
. In other words — surrender. Take your 100mb bundle, and make twenty 5mb bundles from it. Honestly — that's the only possible way to handle your application if it got big — create a pack of smaller apps from it.
But there is one thing you should know right now: whatever option you choose, it's an implementation detail, while we are looking for something more reliable.
The Truth about Code Splitting
The truth about code splitting is that it's nature is TIME SEPARATION. You are not just splitting your code, you are splitting it in a way where you will use as little as possible in a single point of time.
Just don't ship the code you don't need right now. Get rid of it.
Easy to say, hard to do. I have a few heavy, but not adequately split applications, where any page loads like 50% of everything. Sometimes code splitting
becomes code separation
, I mean — you may move the code to the different chunks, but still, use it all. Recal that "Just don't ship the code you don't need right now",– I needed 50% of the code, and that was the real problem.
Sometimes just adding import
here and there is not enough. Till it is not time separation, but only space separation — it does not matter at all.
There are 3 common ways to code split:
- Just dynamic
import
. Barely used alone these days. It's more about issues with tracking a state. Lazy
Component, when you might postpone rendering and loading of a React Component. Probably 90% of "react code splitting" these days.- Lazy
Library
, which is actually.1
, but you will be given a library code via React render props. Implemented in react-imported-component and loadable-components. Quite useful, but not well known.
Component Level Code Splitting
This one is the most popular. As a per-route code splitting, or per-component code splitting. It's not so easy to do it and maintain good perceptual results as a result. It's death from Flash of Loading Content
.
The good techniques are:
- load
js chunk
anddata
for a route in parallel. - use a
skeleton
to display something similar to the page before the page load (like Facebook). prefetch
chunks, you may even use guess-js for a better prediction.- use some delays, loading indicators,
animations
andSuspense
(in the future) to soften transitions.
And, you know, that's all about perceptual performance.
Image from Improved UX with Ghost Elements
That doesn't sound good
You know, I could call myself an expert in code splitting — but I have my own failures.
Sometimes I could fail to reduce the bundle size. Sometimes I could fail to improve resulting performance, as long as the _more_ code splitting you are introducing - the more you spatially split your page - the more time you need to _reassemble_ your page back
*. It's called a loading waves.
- without SSR or pre-rendering. Proper SSR is a game changer at this moment.
Last week I've got two failures:
- I've lost in one library comparison, as long as my library was better, but MUCH bigger than another one. I have failed to "1. Write less code".
- optimize a small site, made in React by my wife. It was using route-based component splitting, but the
header
andfooter
were kept in the main bundle to make transitions more "acceptable". Just a few things, tightly coupled with each other skyrocketed bundle side up to 320kb(before gzip). There was nothing important, and nothing I could really remove. A death by a thousand cuts. I have failed to Ship less code.
React-Dom was 20%, core-js was 10%, react-router, jsLingui, react-powerplug… 20% of own code… We are already done.
The solution
I've started to think about how to solve my problem, and why common solutions are not working properly for my use case.
What did I do? I've listed all crucial location, without which application would not work at all, and tried to understand why I have the rest.
It was a surprise. But my problem was in CSS. In vanilla CSS transition.
Here is the code
- a control variable —
componentControl
, eventually would be set to somethingDisplayData
should display. - once value is set —
DisplayData
become visible, changingclassName
, thus triggering fancy transition. SimultaneuslyFocusLock
become active makingDisplayData
a modal.
<FocusLock enabled={componentControl.value} // ^ it's "disabled". When it's disabled - it's dead. > {componentControl.value && <PageTitle title={componentControl.value.title}/>} // ^ it's does not exists. Also dead <DisplayData data={componentControl.value} visible={componentControl.value !== null} // ^ would change a className basing on visible state /> // ^ that is just not visible, but NOT dead </FocusLock>
I would like to code split this piece as a whole, but this is something I could not do, due to two reasons:
- the information should be visible immediately, once required, without any delay. A business requirement.
- the information "chrome" should exist before, to property handle transition.
This problem could be partially solved using CSSTransitionGroup or recondition. But, you know, fixing one code adding another code sounds weird, even if actually enought. I mean adding more code could help in removing even more code. But… but...
There should be a better way!
TL;DR — there are two key points here:
DisplayData
has to be mounted, and exists in the DOM prior.FocusLock
should also exist prior, not to causeDisplayData
remount, but it's brains are not needed in the beginning.
So let's change our mental model
Batman and Robin
Let assume that our code is Batman and Robin. Batman can handle most the bad guys, but when he can't, his sidekick Robin comes to the rescue..
Once again Batman would engage the battle, Robin will arrive later.
This is Batman:
+<FocusLock
- enabled={componentControl.value}
+>
- {componentControl.value && <PageTitle title={componentControl.value.title}/>}
+ <DisplayData
+ data={componentControl.value}
+ visible={componentControl.value !== null}
+ />
+</FocusLock>
This is his sidekick, Robin::
-<FocusLock
+ enabled={componentControl.value}
->
+ {componentControl.value && <PageTitle title={componentControl.value.title}/>}
- <DisplayData
- data={componentControl.value}
- visible={componentControl.value !== null}
- />
-</FocusLock>
Batman and Robin could form a TEAM, but they actually, are two different persons.
And don't forget — we are still talking about code splitting. And, in terms of code splitting, where is the sidekick? Where is Robin?
in a sidecar. Robin is waiting in a sidecar chunk.
Sidecar
Batman
here is all visual stuff your customer must see as soon as possible. Ideally instantly.Robin
here is all logic, and fancy interactive features, which may be available a second after, but not in the very beginning.
It would be better to call this a vertical code splitting where code branches exist in a parallel, in opposite to a common horizontal code splitting where code branches are cut.
In some lands, this trio was known as replace reducer
or other ways to lazy load redux logic and side effects.
In some other lands, it is known as "3 Phased" code splitting
.
It's just another separation of concerns, applicable only to cases, where you can defer loading some part of a component, but not another part.
image from Building the New facebook.com with React, GraphQL and Relay, whereimportForInteractions
, orimportAfter
are thesidecar
.
And there is an interesting observation — while Batman
is more valuable for a customer, as long as it's something customer might see, he is always in shape… While Robin
, you know, he might be a bit overweight, and require much more bytes for living.
As a result — Batman alone is something much be bearable for a customer — he provides more value at a lower cost. You are my hero Bat!
What could be moved to a sidecar:
- majority of
useEffect
,componentDidMount
and friends. - like all Modal effects. Ie
focus
andscroll
locks. You might first display a modal, and only then make Modal modal, ie "lock" customer's attention. - Forms. Move all logic and validations to a sidecar, and block form submission until that logic is loaded. The customer could start filling the form, not knowing that it's only
Batman
. - Some animations. A whole
react-spring
in my case. - Some visual stuff. Like Custom scrollbars, which might display fancy scroll-bars a second later.
Also, don't forget — Every piece of code, offloaded to a sidecar, also offload things like core-js poly- and ponyfills, used by the removed code.
Code Splitting can be smarter than it is in our apps today. We must realize there is 2 kinds of code to split: 1) visual aspects 2) interactive aspects. The latter can come a few moments later. Sidecar
makes it seamless to split the two tasks, giving the perception that everything loaded faster. And it will.
The oldest way to code split
While it may still not be quite clear when and what a sidecar
is, I'll give a simple explanation:
Sidecar
is ALL YOUR SCRIPTS. Sidecar is the way we codesplit before all that frontend stuff we got today.
I am talking about Server Side Rendering(SSR), or just plain HTML, we all were used to just yesterday. Sidecar
makes things as easy as they used to be when pages contained HTML and logic lived separately in embeddable external scripts (separation of concerns).
We had HTML, plus CSS, plus some scripts inlined, plus the rest of the scripts extracted to a .js
files.
HTML
+CSS
+inlined-js
were Batman
, while external scripts were Robin
, and the site was able to function without Robin, and, honestly, partially without Batman (he will continue the fight with both legs(inlined scripts) broken). That was just yesterday, and many "non modern and cool" sites are the same today.
If your application supports SSR — try to disable js and make it work without it. Then it would be clear what could be moved to a sidecar.
If your application is a client-side only SPA — try to imagine how it would work, if SSR existed.
For example — theurge.com, written in React, is fully functional without any js enabled.
There is a lot of things you may offload to a sidecar. For example:
- comments. You might ship code to
display
comments, but notanswer
, as long as it might require more code(including WYSIWYG editor), which is not required initially. It's better to delay a commenting box, or even just hide code loading behind animation, than delay a whole page. - video player. Ship "video" without "controls". Load them a second later, them customer might try to interact with it.
- image gallery, like
slick
. It's not a big deal to draw it, but much harder to animate and manage. It's clear what could be moved to a sidecar.
Just think what is essential for your application, and what is not quite...
Implementation details
(DI) Component code splitting
The simplest form of sidecar
is easy to implement — just move everything to a sub component, you may code split using an "old" ways. It's almost a separation between Smart and Dumb components, but this time Smart is not contaniting a Dumb one — it's opposite.
const SmartComponent = React.lazy( () => import('./SmartComponent'));
class DumbComponent extends React.Component {
render() {
return (
<React.Fragment>
<SmartComponent ref={this} /> // <-- move smart one inside
<TheActualMarkup /> // <-- the "real" stuff is here
</React.Fragment>
}
}
That also requires moving initialization code to a Dumb one, but you are still able to code-split the heaviest part of a code.
Can you see aparallel
orvertical
code splitting pattern now?
useSidecar
Building the New facebook.com with React, GraphQL and Relay, I've already mentioned here, had a concept of loadAfter
or importForInteractivity
, which is quite alike sidecar concept.
In the same time, I would not recommend creating something like useSidecar
as long you might intentionally try to use hooks
inside, but code splitting in this form would break rule of hooks.
Please prefer a more declarative component way. And you might use hooks
inside SideCar
component.
const Controller = React.lazy( () => import('./Controller'));
const DumbComponent = () => {
const ref = useRef();
const state = useState();
return (
<>
<Controller componentRef={ref} state={state} />
<TheRealStuff ref={ref} state={state[0]} />
</>
)
}
Prefetching
Dont forget — you might use loading priority hinting to preload or prefetch sidecar
and make it shipping more transparent and invisible.
Important stuff — prefetching scripts would load it via network, but not execute (and spend CPU) unless it actually required.
SSR
Unlike normal code splitting, no special action is required for SSR. Sidecar
might not be a part of the SSR process and not required before hydration
step. It's could be postponed "by design".
Thus — feel free to use React.lazy
(ideally something without Suspense
, you don't need any failback(loading) indicators here), or any other library, with, but better without SSR support to skip sidecar chunks during SSR process.
The bad parts
But there are a few bad parts of this idea
Batman is not a production name
While Batman
/Robin
might be a good mind concept, and sidecar
is a perfect match for the technology itself — there is no "good" name for the maincar
. There is no such thing as a maincar
, and obviously Batman
, Lonely Wolf
, Solitude
, Driver
and Solo
shall not be used to name a non-a-sidecar part.
Facebook have used display
and interactivity
, and that might be the best option for all of us.
If you have a good name for me — leave it in the comments
Tree shaking
It's more about the separation of concerns from bundler point of view. Let's imagine you have Batman
and Robin
. And stuff.js
stuff.js
export * from `./batman.js` export * from `./robin.js`
Then you might try component based code splitting to implement a sidecar
main.js
import {batman} from './stuff.js' const Robin = React.lazy( () => import('./sidecar.js')); export const Component = () => ( <> <Robin /> // sidecar <Batman /> // main content </> )
sidecar.js
// and sidecar.js... that's another chunk as long as we `import` it import {robin} from './stuff.js' .....
In short — the code above would work, but will not do "the job".
- if you are using only
batman
fromstuff.js
— tree shaking would keep only it. - if you are using only
robin
fromstuff.js
— tree shaking would keep only it. - but if you are using both, even in different chunks — both will be bundled in a first occurrence of
stuff.js
, ie the main bundle.
Tree shaking is not code-splitting friendly. You have to separate concerns by files.
Un-import
Another thing, forgotten by everybody, is the cost of javascript. It was quite common in the jQuery era, the era of jsonp
payload to load the script(with json
payload), get the payload, and remove the script.
Nowadays we all import
script, and it will be forever imported, even if no longer needed.
As I said before — there is too much JS, and sooner or later, with continuous navigation you will load all of it. We should find a way to un-import no longer need chunk, clearing all internal caches and freeing memory to make web more reliable, and not to crush application with out of memory exceptions.
Probably the ability to un-import
(webpack could do it) is one of the reasons we should stick with component-based API, as long as it gives us an ability to handle unmount
.
So far — ESM modules standards have nothing about stuff like this — nor about cache control, nor about reversing import action.
Creating a sidecar-enabled Library
By today there is only one way to create a sidecar
-enabled library:
- split your component into parts
- expose a
main
part andconnected
part(not to break API) viaindex
- expose a
sidecar
via a separated entry point. - in the target code — import the
main
part and thesidecar
— tree shaking should cut aconnected
part.
This time tree shaking should work properly, and the only problem — is how to name the main
part.
main.js
export const Main = ({sidecar, ...props}) => (
<div>
{sidecar}
....
</div>
);
connected.js
import Main from './Component';
import Sidecar from './Sidecar';
export const Connected = props => (
<Main
sidecar={<Sidecar />}
{...props}
/>
);
index.js
export * from './Main';
export * from './Connected';
sidecar.js
import * from './Sidecar';
In short, the change could be represented via a small comparison
//your app BEFORE
import {Connected} from 'library'; //
// -------------------------
//your app AFTER, compare this core to `connected.js`
import {Main} from 'library';
const Sidecar = React.lazy(import( () => import('library/sidecar')));
// ^ all the difference ^
export SideConnected = props => (
<Main
sidecar={<Sidecar />}
{...props}
/>
);
// ^ you will load only Main, Sidecar will arrive later.
Theoretically dynamic import
could be used inside node_modules, making assemble process more transparent.
Anyway — it's nothing more thanchildren
/slot
pattern, so common in React.
The future
Facebook
proved that the idea is right. If you haven't seen that video — do it right now. I've just explained the same idea from a bit different angle (and started writing this article a week before F8 conference).
Right now it requires some code changes to be applied to your code base. It requires a more explicit separation of concerns to actually separate them, and let of codesplit not horizontally, but vertically, shipping lesser code for a bigger user experience.
Sidecar
, probably, is the only way, except old school SSR, to handle BIG code bases. Last chance to ship a minimal amount of code, when you have a lot of it.
It could make a BIG application smaller, and a SMALL application even more smaller.
10 years ago the medium website was "ready" in 300ms, and was really ready a few milliseconds after. Today seconds and even more than 10 seconds are the common numbers. What a shame.
Let's take a pause, and think — how we could solve the problem, and make UX great again...
Overall
- Component code splitting is a most powerful tool, giving you the ability to completely split something, but it comes with a cost — you might not display anything except a blank page, or a skeleton for a while. That's a horizontal separation.
- Library code splitting could help when component splitting would not. That's a horizontal separation.
- Code, offloaded to a sidecar would complete the picture, and may let you provide a far better user experience. But would also require some engineering effort. That's a vertical separation.
Let's have a conversation about this.
Stop! So what about the problems you tried to solve?
Well, that was only the first part. We are in the endgame now, it would take a few more weeks to write down the second part of this proposal. Meanwhile… get in the sidecar!