DEV Community

Cover image for πŸš— Sidecar for a Code splitting
Anton Korzunov
Anton Korzunov

Posted on • Edited on

πŸš— Sidecar for a Code splitting

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.

As long as we're discussing it, you may want to make sure you're up on the latest and greatest when it comes to React code-splitting in 2019. Or just read about some implementation details.

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.

seee yaa

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. Recall 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.

loading waves

Last week I 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 me - the problem was in CSS. In vanilla CSS transition I've used for a smoother UI, and the way I implemented it. Long story short - an underlying DOM node has to exist before transition animation.

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} 
 // ^ initially it's "disabled". And when it's disabled - it's dead.
>
  {componentControl.value && <PageTitle title={componentControl.value.title}/>}
  // ^ it's does not exists. Dead-dead
  <DisplayData
    data={componentControl.value}
    visible={componentControl.value !== null}
    // ^ would change a className basing on visible state
  />
  // ^ that is just not visible, but EXISTS
</FocusLock>
Enter fullscreen mode Exit fullscreen mode

I would like to code split this piece as a whole, but this is something I cannot do, due to two reasons:

  1. the information should be visible immediately, once required, without any delay. A business requirement. So it's better not to code split information.
  2. the information "skeleton" should exist before, to property handle CSS transition.

This problem could be partially solved using CSSTransitionGroup or recondition - first create hidden, then apply a visible classname - 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, to contain DisplayData, 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>
Enter fullscreen mode Exit fullscreen mode

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>
Enter fullscreen mode Exit fullscreen mode

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?

Robin is in sidecar

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 as they needed.
  • 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 code spliting

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 (and has a secret abs)... 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.
  • Custom Selects - they are naturally split into Batman(Input) and Robin(Dropdrown). Custom Calendars or any other UI component with displays another (the biggest and most complex) part or click/hover - are the same.
  • 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. πŸ€·β€β™‚οΈ Designers πŸ€·β€β™‚οΈ

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.

batman and robin

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 was 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, the 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>
  } 
}
Enter fullscreen mode Exit fullscreen mode

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]} />
  </>
 )
}
Enter fullscreen mode Exit fullscreen mode

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

export * from `./batman.js`
export * from `./robin.js`
Enter fullscreen mode Exit fullscreen mode

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
  </>
)

// and sidecar.js... that's another chunk as long as we `import` it
import {robin} from './stuff.js'
.....
Enter fullscreen mode Exit fullscreen mode

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';

// -------------------------

//your app BEFORE
import {Connected} from 'library'; //

// -------------------------

//your app AFTER, compare 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.
Enter fullscreen mode Exit fullscreen mode

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 Final Form

With all the principles listed above the final sidecar form is:

import {Main} from 'library';
const Sidecar = React.lazy(import(/* webpackPrefetch: true */ () => import('library/sidecar')));

export SideConnected = ({enabled, props}) => (
  <Main
    sidecar={enabled && <Sidecar />}
    {...props}
  />
);
Enter fullscreen mode Exit fullscreen mode

It prefetches sidecar chunk and uses not when component just "used", but when it used in a "active" form (if that form exists).

Without extraction of "active form" sidecar would improve Time-To-Render, separating it from Time-To-Interactive, keeping the second a bit delayed as long as "interactivity" would be loaded by the main bundle itself.

This "a bit" could be a whole time required to load main chunk and render your application for the first time.

Keep in mind - extracting small "cars", required just after initial rendering could be not the best idea. In my case I was able to "extract" almost 70% of the code greatly improving Time-To-Render.

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 codebase. 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 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...

should vs could

Overall

sidecar provides time and/or space separation. You can import all scripts you need a bit later... a bit later using dynamic import, or you can require them, when you need them. In the second time you will make things simpler, and more synchornios but still be able to save some initial bundle starting time, deferring modules evaluation example.

// time and space separation
const ImportSidecar = sidecar( () => import("./sidecar"));

export function ComponentCombination(props) {
  return (
    <ComponentUI
      {...props}
      sideCar={RequireSideCar}
    />
  );
}


// only time separation
const RequireSideCar = (props: any) => {
  const SideCar = require('./sidecar').default;
  return <SideCar {...props} />;
};

export function ComponentCombination(props) {
  return (
    <ComponentUI
      {...props}
      sideCar={RequireSideCar}
    />
  );
}
Enter fullscreen mode Exit fullscreen mode
  • 1. 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.
  • 2. Library code-splitting could help when component splitting would not. That's a horizontal separation.
  • 3. 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?

react-focus-lock, react-focus-on and react-remove-scroll have implemeneted this pattern.

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...

Top comments (3)

Collapse
 
jovidecroock profile image
Jovi De Croock

In your last example why do you load library/sidecar lazily but in your library use it as a static import?
Not really fully on that boat yet, great article by the way. Love reading your articles since every concept has a well-defined example attached to it.

Collapse
 
thekashey profile image
Anton Korzunov • Edited

It was more about:

  1. You was exporting a Thing from your library
  2. You separated a thing to Main and Sidecar
  3. You are exporting Main, just the one part, and the old Thing assembled back from new pieces. Library public API is not changed. This is no more than a minor bump.
  4. You are exporting a Sidecar via another endpoint
  5. ....
  6. You are assembling a Thing in a user space from Main and lazy Sidecar.

Technically you may keep import in a library code, but you will loose control on chunk name and prefetching.

As I said - this is a subject to complete and argue about.

Collapse
 
pierre profile image
Pierre-Henry Soria ✨

Amazing read! πŸ‘ Thanks Anton! πŸ’―