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:


  1. Just dynamic import. Barely used alone these days. It's more about issues with tracking a state.
  2. Lazy Component, when you might postpone rendering and loading of a React Component. Probably 90% of "react code splitting" these days.
  3. 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 and data 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 and Suspense(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 and footer 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 something DisplayData should display.
  • once value is set — DisplayData become visible, changing className, thus triggering fancy transition. Simultaneusly FocusLock become active making DisplayData 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:


  1. the information should be visible immediately, once required, without any delay. A business requirement.
  2. 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 cause DisplayData 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.

phase 3


image from Building the New facebook.com with React, GraphQL and Relay, where importForInteractions, or importAfter are the sidecar.

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 and scroll 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 not answer, 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 a parallel or vertical 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 from stuff.js — tree shaking would keep only it.
  • if you are using only robin from stuff.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 and connected part(not to break API) via index
  • expose a sidecar via a separated entry point.
  • in the target code — import the main part and the sidecar — tree shaking should cut a connected 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 than children/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!

Комментарии (0)