Posted on • Originally published atcamillehdl.dev
Ship modern JavaScript with Rollup
Thispost is a few months old, but it still gets a few hits on my blog everyday so maybe it can help people.
Using ES2017 (and newer) syntax in our codebase is one thing, sending it to our users is another.
Chances are, if we have to support IE11 and/or use@babel/preset-env
and/or ship a single bundle (even with code-splitting), we're probably sending ES5 to everybody, even if their browser is perfectly capable of understandingclasses orasync/await.
This is suboptimal, becausewe're sending more code than needed and, long term,we'll miss out on some "free" performance gains.
You can read more about the peformance of ES6 features compared to their ES5 transpiled versions on theSix Speed project. The status quo isn't set in stone, as browser vendors regularly improve the performance of new syntax (seethis andthis).
Moreover, even if all ourpreset-env
targets are up-to-date, Babelmay still be transpiling some syntax because of edge-cases. Besides, our dependencies are probably transpiled to ES5 anyway.
A better thing to do would be toship transpiled ES5 to legacy browsers andas much ES2017 as possible to modern browsers.
The steps we need to take to achieve this are:
- create 2 distinct bundles
- use
preset-modules
instead ofpreset-env
for our modern build, - configure terser to output ES2017
- use
- load each bundle in the correct browsers.
You can see a working examplein this repo.
Bundling
If you're using Rollup, you can already output ES modules or a fallback out-of-the-box.
To create each build with a different config however, we have to add a bit of configuration.
One way to do this could be to use an environment variableROLLUP_BUILD_TYPE
, in npm scripts.
Rollup and Terser configuration
// package.json{/** ... **/"scripts":{"build":"npm-run-all --parallel\"build:*\"","build:legacy":"env ROLLUP_BUILD_TYPE=\"legacy\" rollup -c","build:modern":"env ROLLUP_BUILD_TYPE=\"modern\" rollup -c"},"devDependencies":{"npm-run-all":"...","rollup":"..."}/** ... **/}
// rollup.config.jsexportdefault()=>{constbuildType=typeofprocess.env.ROLLUP_BUILD_TYPE!=="undefined"?process.env.ROLLUP_BUILD_TYPE:"modern";return{input:["./src/index.jsx"],output:buildType==="modern"?{dir:`/public/js/system/`,format:"system"}:{dir:`/public/js/esm/`,format:"esm"},plugins:[/** ... **/babel({/** * Uncomment to ignore node_modules. This will accelerate yur build, * but prevent you from using modern syntax in your dependencies */// exclude: "node_modules/**"}),terser({compress:{unused:false,collapse_vars:false},sourcemap:true,ecma:buildType==="legacy"?5:2017,safari10:true,})/** ... **/]};};
Notice howwe didn't exclude node_modules/ from Babel. This will let us optimally transpile our dependencies if they export modern syntax. More on that later.
Terser is a minifier which needs to be told which version of the language we are using.
Babel configuration
Babel 7 can be configured with a function instead of a static object. We can take advantage of this feature in order to write conditional and annotated code.
Remember that our goal is to transpile as little as possible. In this example, I include a preset for react as well as plugins forES2020 features, that will still be used for a while even in modern browsers.
// in babel.config.cjsmodule.exports=function(api){api.cache.invalidate(()=>[process.env.NODE_ENV,process.env.ROLLUP_BUILD_TYPE].join("-"));constenvironment=api.env();constmodern=process.env.ROLLUP_BUILD_TYPE==="modern";/** * Will be used for the legacy build */constpresetEnv=["@babel/preset-env",{modules:false,targets:{browsers:[">0.25%","not op_mini all"],},},];/** * Will be used for the modern build */constpresetModule=["@babel/preset-modules",{loose:true,},];constalwaysUsedPresets=["@babel/preset-react"];constalwaysUsedPlugins=["@babel/plugin-syntax-dynamic-import","@babel/plugin-proposal-optional-chaining","@babel/plugin-proposal-nullish-coalescing-operator",];/** * Only loaded in the legacy build */constlegacyPlugins=["@babel/plugin-proposal-object-rest-spread"];constproductionConfig=modern==="modern"?{/** * Modern build */presets:[presetModule,...alwaysUsedPresets],plugins:[...alwaysUsedPlugins],}:{/** * Legacy build */presets:[presetEnv,...alwaysUsedPresets],plugins:[...alwaysUsedPlugins,...legacyPlugins],};constdevelopmentConfig=modern==="modern"?{/** * Modern build */presets:[presetModule,...alwaysUsedPresets],plugins:[...alwaysUsedPlugins],}:{/** * Legacy build */presets:[presetEnv,...alwaysUsedPresets],plugins:[...alwaysUsedPlugins,...legacyPlugins],};return{env:{production:productionConfig,development:developmentConfig,test:{/** * Chances are tests will run in node **/presets:["@babel/preset-env",...alwaysUsedPresets],plugins:[...alwaysUsedPlugins,...legacyPlugins],},},};};
We didn't use the .mjs alternative because it only works when babel is loaded asynchonously.
preset-modules
will transpile less code thanpreset-env
. Readthe documentation to understand why.
Now that we have our bundles, we need a way to load them in their targets.
The module/nomodule pattern
This technique (from2017) utilizes<script type="module">
support as a breakpoint between legacy browsers (mostly IE nowadays) and "evergreen" browsers (Chrome, Firefox, Safari, Edge, ...).
This gives us a way to send a modern bundle to modern browsers, and a legacy bundle to old browsers.
For a long time, things weren't as simple assome browsers did support modules, but didn't supportimport()
. This has now been delt wih though, and<script type="module">
can be safely used.
The pattern:
<scripttype="module">import("/js/esm/main.js").then((module)=>{/** ... **/});</script><scriptnomodulesrc="/js/legacy/main.js"></script>
The future
While this post is about ES2017, the techniques explained here can be updated later to use newer versions of the language, as long as browser support keeps up.
One important limitation to note is that most third-party libraries stilldon't export untranspiled code. Unfortunately, this means the majority of the code you ship will still be ES5 for now.
However, if you stop excluding node_modules/ from your Babel config, and more people do the same, things might improve soon enough.
Top comments(0)
For further actions, you may consider blocking this person and/orreporting abuse