Serving Modern and Legacy Bundle

As time progresses, your product becomes rich with features but your app becomes slower now. Especially when you need to support legacy browsers like IE 11 because there are STILL people using it. We need to transpile and polyfill our code so your customers on older browsers can still be happy when they use your product. This results in generating huge bundles that includes stuffs 95% of your users don’t need which impacts performance negatively. We want to stay fast and push only the codes that users truly need. So I generate 1 bundle for modern browser and another 1 for legacy browser. How do I let the browsers decide which one to use.

type=“module”

HTML introduced a way to use JavaScripts module directly on the page. You can literally use import statement to import another module in the browser without using Webpack or Rollup.

html
<script type=“module”>
	import { sendLog } from ‘./utils.mjs';
  sendLog(“WOW”); 
</script>

About the same time a new attribute for script tag called nomodule is added. This indicates if this browser does not support module then use this script tag that has nomodule. So the combination of these 2 will be:

html
<script type=“module” src=“/dist/modern/app.js"></script>
<script type=“text/javascript” src="/dist/legacy/app.js” nomodule></script>

ES modules are supported in these browsers:

  • Safari 10.1
  • Chrome 61
  • Firefox 60
  • Edge 16

You can learn more about it on Can I Use. So now we have a way for browsers to make choose which one to use on the fly without us coding much. Well actually we need to configure Webpack or build tool of your choice to output 2 sets of bundles. ‘

Babel

Typical babel config could look something like:

json
{
  plugins: [
      “@babel/plugin-syntax-dynamic-import”,
      “@babel/plugin-transform-destructuring”,
      “@babel/plugin-proposal-object-rest-spread”,
      “@babel/plugin-proposal-class-properties”,
      “@babel/plugin-proposal-export-default-from”,
      “@babel/plugin-proposal-export-namespace-from”,
  ],
  presets: [
      [
          “@babel/preset-env”,
          {
              “targets”: target,
              useBuiltIns: “entry”,
              corejs: {
                  version: 3.2,
              },
              modules: false,
          }
      ]
  ],
  configFile: false,
};

The above config won’t work because the variable target is not defined yet. My modern target is:

json
{
  “chrome”:61,
  “edge”:16,
  “firefox”:60,
  “safari”:10.1,
  “opera”:48,
}

And the legacy is:

json
{
  “browsers”: [
      “> 0.5%”,
      “last 1 version”,
      “not dead”,
  ]
}

Those browsers defined in modern config are browsers that support type=“module” so by passing this to babel we should have less polyfills and less transpilation done on our code. I have these configs inside my webpack.config.js instead of .babelrc just so it’s easier to configure for me.

The way I tell Webpack which one to transpile is by passing environment variable from the command line. In my package.json I have:

json
“scripts”: {
	“build:legacy”: “BUILD_MODERN=‘false’ webpack —config webpack.config.js”,
	“build:modern”: “BUILD_MODERN=‘true’ webpack —config webpack.config.js”,
}

Finally in your webpage.config.js, you can configure your babel configuration in babel loader like:

json
{
  “test”: /\.js$/,
  “include”: [
    path.resolve(__dirname, “./js”),
  ],
  “use”: {
    “loader”: “babel-loader”,
    “options”: process.env.BUILD_MODERN == ‘true’ ? babelModern : babelLegacy
  }
}

process.env.BUILD_MODERN == ‘true’ is what determines what value you passed when you run webpack and use the correct babel configuration accordingly. You should also use it to determine where your output files should be. You can check out more about babel config here. To make your build faster you can run these 2 scripts at the same time using npm-run-all.

bash
npm install npm-run-all -D
run-p build:modern build:legacy

You will need to be careful with this approach if you have CSS and not doing server-side rendering because you will need to some JavaScripts hacks to tell the browser which CSS to use. Using one of modern or legacy to generate the CSS will work unless you are dynamic importing your components in which case webpack won’t be able to find the correct chunk that has the CSS.

Bonus

You probably know you can use link preload to tell the browser to start preloading JavaScripts without executing them:

html
<link rel=“preload” href=“main.js” as="script">

preload doesn’t work if you are loading your JavaScripts files using type=‘module’, thus modulepreload is here to help.

html
<link rel=“modulepreload” href=“main.js”>

You can read more about modulepreload Preloading modules  |  Web  |  Google Developers and the support Can I use… Support tables for HTML5, CSS3, etc. Please note that from my personal testing, if you use modulepreload technique to preload your JavaScripts and later on you use dynamic import to load the same file, it does not reuse what has been downloaded but instead it fetches it again. It does not work the same way as preload where it wouldn’t send another request to fetch again.

If you like what you read please share on Twitter and let me know what you think, thank you!

Cover photo credit goes to unsplash-logoKolleen Gladden