🛳 Optimising JS Delivery
Tags: webdev, codesplitting, javascript
Published: 2019-10-04 01:21:44

---
title: 🛳 Optimising JS Delivery
published: true
description: Getting the maximum from low-level script delivery system
tags: webdev, code-splitting, javascript
series: A split second later 
cover_image: https://thepracticaldev.s3.amazonaws.com/i/ui1jj8fdrpqd2ojiy94z.jpg
---

...in the previous part, we somehow made our codebase smaller, or just reduced the amount of code needed at a `single point of time` - implemented the basic idea behind code splitting. And now we have to somehow send our bundle from our Server to our Client.

Not a big deal? 😉

To be honest - loading JavaScript is not a big deal, but making it _efficiently_ - is.

And everything is bound only to two moments:
- optimizing network utilization. Ie “transferring” scripts in an efficient way.
- optimizing script loading and execution, not blocking the main thread much.
- and shipping the "right" scripts to the client, not shipping the “wrong” ones.

# Network Utilization

__Rule number 1__ - never use `<script src="..." />`. It's will block everything till it's loaded and executed. It's the slowest possible way, however the most popular.

__Rule number 2__ - always use __defer__ attribute to load all scripts in the parallel, and execute them in the "safe" order and time - after your page is ready to safely execute any scripts - after `"DOMReady"`.
`Defer` mode has another good power - it will download scripts in a random order, but execute in the "right". However - only your bundler knows the right order, and only it controls is, so - that's not a benefit if you use Webpack for example.
Another drawback is the __network priority__ of such scripts - they are deferred, so nobody would complain if you will get them a second later - as a result they would be loaded in a lowest network priority.

__Rule number 3__ - try to use __async__ mode, but be careful - as long as your scripts might be executed when your page is not yet ready. But it's ok to do some job, like load extra dependencies, and continue job after ‘DOMReady’, and you have to keep that in mind. Remember jQuery's `$.ready`?
However, async scripts are also are "not required right here right now" - `async` is a clear signal - I don't need it now - so network priority is still low.

> For example in Chrome, CSS loaded in typical fashion via the `<link>` element in the `<head>` will be assigned a priority of highest, as it blocks rendering - HTML could not be displayed without matching CSS. Images in the viewport may be assigned a priority of high, whereas images outside the viewport may be assigned a priority of low. A `<script>` loaded at the end of the document may receive a priority assignment of medium or low, but this can be influenced by `defer` and `async`.

Here is a [table Addy Osmany created](https://addyosmani.com/blog/script-priorities/) to help you understand how priority depends on your actions:

![Script Ordering](https://thepracticaldev.s3.amazonaws.com/i/5uf8j2gn3ofy2lc4z9ev.png) 


The easiest way to handle this would be to use [Priority Hints](https://developers.google.com/web/updates/2019/02/priority-hints), but unfortunately, they are [not supported](https://www.chromestatus.com/feature/5273474901737472) just everywhere, even in Chrome they are still behind a flag.

So there is only one way to "fix" scripts, and here it is:

__Rule number 4__ - try to use `link preload` to make your scripts more _important_ than images, thus make their loading faster and solve [the uncanny valley of SSR](https://addyosmani.com/blog/rehydration/) problem, when everything is “visually” ready, except the page brains 🧠, still transferring over the air.

Thus, the best advice I could give you - load all your scripts using `async`, and pair `<scripts` tags with `<link prefetch` tags to make it faster.

Easy, until you will try to ship even less code to the browser, I mean _"ship the right scripts to the client"_, which is also known as a `double bundling`, `module/nomodule`, or `modern/legacy` approach.

What could be wrong this time?

# Shipping less code

“Double bundle” shipping is something everybody is talking about, but just a few uses in production. The reasons are usually the same - it's freaking hard.

> JFYI: `module/modern` is a bundle created for "new" browsers, with all cool features you can use out of the box, while `nomodule/legacy` is a bundle for old browsers, which are still around - like IE11.

Let's start with some reasons to __use it__. With `modern` bundles...
- you don't have to __ship polyfils__, which is not a deal-breaker for _big fat apps_, which would not gain measurable benefit by reducing their size by 100kb, but a greater deal for more fit ones (read: almost for all _normal_ apps).
- you don't have to transpile everything __down to es5__. `classes` would be still `classes` (and not that overweighted templates they become in es5) - `async/await` and `generators` would keep their form, not become these huge es5-compatible `switch/case` based state machines.
- `css` would need less prefixing, but CSS is not a big problem usually.

Let me cite @pastelsky article [Smart Bundling: How To Serve Legacy Code Only To Legacy Browsers](https://www.smashingmagazine.com/2018/10/smart-bundling-legacy-code-browsers/)

![Bundle separation](https://thepracticaldev.s3.amazonaws.com/i/zchn99gdhap2816on1nd.png)

So _modern_ bundles could be slimmer and sometimes even faster. Not much slimmer, and not much faster, sometimes almost not measurable... 
Unfortunately, nothing is under you control, and the problem here is NPM modules, which might be a quite big part of your application, and which are already in ES5, so you can't ship them in a modern way - there are no "modern" sources provided.

> For the Instagram it ended as 5.7% size reduction. [Read about their experience](https://instagram-engineering.com/making-instagram-com-faster-code-size-and-execution-optimizations-part-4-57668be796a8) with shipping modern bundles.

Jason Miler has got a great article about it - [Enabling Modern JavaScript on npm](https://jasonformat.com/enabling-modern-js-on-npm/), and might one day, one bright day, our expectation about lowest common denominator (es5) would change.

![Your code vs NPM code](https://thepracticaldev.s3.amazonaws.com/i/o97slmz6sbqqdvqhmuzo.png)

...but as long as you can get modern bundles almost for free - then why not?


## I can get it almost for free?

Sounds a bit strange, having in mind I've just said - _"it's freaking hard."_. It is, but not because it's hard, but because nobody knows what to do. Or even because everybody knowns - every, like __every__ article, provides different bits of advice. And yes - every article offers some advice, and this one would not be an exclusion.

And there are __3 different ways__ to load `module/nomodule` bundles, [expained here](https://jasonformat.com/modern-script-loading/) in detail.

__1__ - use `module/nomodule`. Obviously.
`<script nomodule/>` would not be loaded if your browser could load `module`, and `<script module />` would not be loaded if your browser could not load it. 99% articles and GitHub issues would say that it's not supported by Safari 10, however, I am writing this text using Safary 13...

And well, there is a project which tracks how it actually works - https://github.com/johnstew/differential-serving - and according to is Safari is not an issue (Edge is)

| Browser          | Version | Browser Test Link                                                                        | Browser Test Results |
|------------------|---------|------------------------------------------------------------------------------------------|----------------------|
| Chrome           | 73      | [View](https://johnstew.github.io/differential-serving/tests/index.html#Chrome-73)       | ✅                    |
| Chrome           | 61      | [View](https://johnstew.github.io/differential-serving/tests/index.html#Chrome-61)       | ✅                    |
| Chrome           | 60      | [View](https://johnstew.github.io/differential-serving/tests/index.html#Chrome-60)       | ✅                    |
| Safari           | 12      | [View](https://johnstew.github.io/differential-serving/tests/index.html#Safari-12)       | ✅                    |
| Safari           | 11.1    | [View](https://johnstew.github.io/differential-serving/tests/index.html#Safari-11)       | ✅                    |
| Safari           | 10.1    | [View](https://johnstew.github.io/differential-serving/tests/index.html#Safari-10-1)     | ⁉️(1)                |
| Firefox          | 66      | [View](https://johnstew.github.io/differential-serving/tests/index.html#Firefox-66)      | ✅                    |
| Firefox          | 60      | [View](https://johnstew.github.io/differential-serving/tests/index.html#Firefox-60)      | ✅                    |
| Firefox          | 59      | [View](https://johnstew.github.io/differential-serving/tests/index.html#Firefox-59)      | ⁉️(2)                |
| MSIE             | 11      | [View](https://johnstew.github.io/differential-serving/tests/index.html#MSIE-11)         | ⁉️(2)                |
| MSEdge           | 18      | [View](https://johnstew.github.io/differential-serving/tests/index.html#MSEdge-18)       | ⁉️(3)                |
| MSEdge           | 16      | [View](https://johnstew.github.io/differential-serving/tests/index.html#MSEdge-16)       | ⁉️(2)                |
| MSEdge           | 15      | [View](https://johnstew.github.io/differential-serving/tests/index.html#MSEdge-15)       | ⁉️(2)                |
| iPhone XS Safari | Latest  | [View](https://johnstew.github.io/differential-serving/tests/index.html#iPhoneXS-Safari) | ✅                    |
| iPhone X Safari  | Latest  | [View](https://johnstew.github.io/differential-serving/tests/index.html#iPhoneX-Safari)  | ✅                    |
| iPhone 8 Safari  | Latest  | [View](https://johnstew.github.io/differential-serving/tests/index.html#iPhone8-Safari)  | ✅                    |
| Pixel 2 Chrome   | Latest  | [View](https://johnstew.github.io/differential-serving/tests/index.html#Pixel2-Chrome)   | ✅                    |
| Galaxy S9 Chrome | Latest  | [View](https://johnstew.github.io/differential-serving/tests/index.html#GalaxyS9-Chrome) | ✅                    |

> 2) MSIE - Downloads both bundles
> 3) Edge downloads legacy bundle and __downloads ESM bundle twice__ 💩
> PS: Edge 18 is also known as [Edge 44.17763](https://en.wikipedia.org/wiki/Microsoft_Edge#EdgeHTML_(2014–2019)_2)


__2__ - serve different bundles from a "smart" server-side, basing on the user agent, as seen at the [Shubham's article](https://www.smashingmagazine.com/2018/10/smart-bundling-legacy-code-browsers/#serving-the-right-bundle-to-users).

```js
router.get('/', async (ctx, next) => {
  const useragent = ctx.get('User-Agent')  
  const isModernUser = matchesUA(useragent)
   const index = isModernUser ? 'dist/modern/index.html', 'dist/legacy/index.html'
   await send(ctx, index);
});
```

That would work, but if you will __load__ a _modern_ script without __module__ attribute - you will lose _stream_ parsing and _off main thread_ compilation, modern browsers could provide for modern scripts (depends on the browser).

So the better, and almost the same way, is to handle this during HTML generation, which is, unfortunately, possible only if you have a non static site. I mean - you do have something like SSR.

```js
function renderPage(request, response) {  
  let html = `<html><head>...`;

  const agent = request.headers.userAgent;
  const isModern = userAgent.isModern(agent);
  if (isModern) {
    html += `
      <link rel=modulepreload href=modern.mjs>
      <script type=module src=modern.mjs></script>
    `;
  } else {
    html += `
      <link rel=preload as=script href=legacy.js>
      <script src=legacy.js></script>
    `;
  }

  response.end(html);
}
```

What if your site is static? Majority of SPAs have "active" backend, but absolutely static frontend assets, served from CDN's. And for these sites, we need a bit different approach.

__3__ - `module` detection in runtime.
Use run time feature detection to discover browser capabilities and... add all scripts dynamically. So no `<script src` anymore - only JS calls.

```js
<link rel="modulepreload" href="/modern.js">  <--- hey?
<script type=module>self.modern=1</script>  
<script>
  $loadjs("/modern.js","/legacy.js")
  function $loadjs(e,d,c){c=document.createElement("script"),self.modern?(c.src=e,c.type="module"):c.src=d,document.head.appendChild(c)}
</script>  

// or
<script> 
  var script = document.createElement('script');
  var prefix = (!('noModule' in check)) ? "/ie11" : "/esm"; 
  script.src = prefix + "/index.js";
  document.head.appendChild(script);
</script>
```

> 💡 dynamically injects scripts are async, but don't have `async` attribute, which raises their network priority

But there is a problem, actually a quite big problem - how to handle code splitting in this case? Any ideas?

# Modern bundles and code splitting

The key idea, and the key problem is: there are two sets of scripts, and you have to load only one - `differential loading` is to load only one, and the best version for you.

First of all - it shall be the same set of scripts, divided in the same way, just content of scripts would be different - optimized for your bundle as much as possible.

Then - it would be different scripts, which shall never interfere with each other, as long as they were built for the different targets.

Finally - `publicPath` might be a bit different. And it's up to you - are you going to use `contentHash` to generate unique names for scripts, or you will just put them into separate directories.

So, yet again - what shall you do:
- __for legacy bundles__ - add `preload` and load `legacy` script
- __for non-Chrome__ - just load using `<script module async`, which will do not require additional `preload` according to the priority table
- __for Chrome(80%)__ - add `modulepreload` to get the maximum from it.

Right now - for the better safety I would recommend using feature detection to decide which bundle you are going to use (variant __3__), accompaniment by `modulepreload` to make it even better for Chrome.

> Keep in mind - `module` is already _hight priority async_ resource, and you don't have to use `preload` to get it _faster_.

Right now only one code-splitting solution - `loadable-components` - has something like double-bundle shipping, but it's still just random code inside [the github issue](https://github.com/smooth-code/loadable-components/issues/365).


## How to get two bundles?
Well, this is the real problem why _differential loading_ is not so popular - the problem with _differential bundling_ itself.

And there are 3 ways to get it right:
- __bundle twice__. Just run `yarn build:es5` one time, and `yarn build:es2015` just after it. In other words - 

```
BABEL_ENV="production:modern" yarn run ...
BABEL_ENV="production:legacy" yarn run ...
```
You can build them in a parallel, or one after another. It would work for any bundler, just don't forget to somehow separate file names (use content hash).

> this is a way any bundler could do the job

- __bundle once__, but twice, which is also known as a `Multi-compiler mode`. Both [webpack](https://webpack.js.org/concepts/targets/#multiple-targets) and [parcel(2)](https://github.com/parcel-bundler/parcel/issues/3011) support it. _Theoretically_, it's more efficient, as long as could share some cache between builds, however - not a fact - both mentioned bundles haven't implement that _shareable_ cache yet, but it's expected in the nearest future.

> this is a smart and modern way to do differential bundling.

- __bundle and rebundle__. Like first bundle everything into `esmodules` target(something with async/await in short, "modern" in short), and then transpile that bundle to a _lower target_. And it would be just a very simple transpilation process - all babel plugin magic, tree shaking, module concatenations and optimizations already applied, not much left. In other words - you don't have to use `babel` for it, but could try something a bit faster:
- [swc](https://github.com/swc-project/swc) - 16x times faster than babel
- [sucrase](https://github.com/alangpierce/sucrase) - 20x times faster than babel, however, it could not be used for this case, as long as it does not handle transpilation down to `es5`.
- [babel](https://babeljs.io)... which could be way faster than `babel`, without any plugins and majority of syntax transformers removed.

> this way is "bundler independent", and also solves the problem with es2015 code in `node_modules`

Solves es2015 code in `node_modules`? Yep - your code would be transpiled as a whole, so it does not matter where you had _modern_ code - in your own code, or `node_modules`.

For now, the only library which handles this process is `devolution`

{% github theKashey/devolution %}

The only downside of `devolution` - it does not work __without__ proper code-splitting - running ше on a big script would just throw out of memory error. So many smaller scripts, not a single big one, is a must.

# Conclusion 
So, how _efficient_ is this technique? Would it really "optimize" JS delivery? Let's measure!

- "vendor" bundle es5: `992.420` bytes
- "vendor" bundle "esmodules": `990.441` bytes (2kb diff)

- "src" bundle es5: `291.115` bytes
- "src" bundle es2015: `271.122 ` bytes (20kb diff)

In short - as it was mentioned above - "modern" bundles could not do much with "non-modern" code in `node_modules`, and might not do much with your own code, if it does not use "modern" constructions, like `async`/`await` for `classes` a lot.
But if you do - it would be a deal breaker. However - every day we are using classes less and less, making using more modern target less and less efficient.

> However, there is a light at the end of the tunnel - look like we could compile npm modules... back to es2015

{% twitter 1179845100270170112 %}

Another light is `babel <-> lebab`. A magic library which transforms your es5 code into es6 (however not 100% safe), and could __uplevel__ existing code.

{% github lebab/lebab %}

***

This all is a micro-optimizations, which is good to have, if you could have them for free and don't have spend much time setting this up, but the real low hanging fruits are growing on other trees:

- _naturally_ __smaller bundles__ are always better than a _minimized_ ones.
- always check for __package duplication__, which is a quite common problem for any big application.
- speaking of duplication - try to remove __babel helpers duplications__ among node_modules, for example using [runtime-compress-loader](https://github.com/theKashey/runtime-compress-loader). This would not affect gzip size, but would reduce _real_ size of your scripts letting them be evaluated faster.
- don't forget that JSX in default settings is using quite verbose `React.createElement` syntax - use [jsx-compress-loader](https://github.com/theKashey/jsx-compress-loader) or (react-local babel)[https://github.com/danya/react-local] to make it more tidy.
- consider SSR to improve FirstContentPaint, and [HTML state](https://dev.to/thekashey/behold-the-html-state-5e8b) to mitigate hydration uncanny valley​ problem.

For example - optimizing CSS delivery could give you more...