Movatterモバイル変換


[0]ホーム

URL:


Skip to content
DEV Community
Log in Create account

DEV Community

Camille Hodoul
Camille Hodoul

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:

  1. create 2 distinct bundles
    1. usepreset-modules instead ofpreset-env for our modern build,
    2. configure terser to output ES2017
  2. 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":"..."}/** ... **/}
Enter fullscreen modeExit fullscreen mode
// 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,})/** ... **/]};};
Enter fullscreen modeExit fullscreen mode

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],},},};};
Enter fullscreen modeExit fullscreen mode

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

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)

Subscribe
pic
Create template

Templates let you quickly answer FAQs or store snippets for re-use.

Dismiss

Are you sure you want to hide this comment? It will become hidden in your post, but will still be visible via the comment'spermalink.

For further actions, you may consider blocking this person and/orreporting abuse

JavaScript & PHP
  • Location
    Grenoble, France
  • Work
    Fullstack developer
  • Joined

More fromCamille Hodoul

DEV Community

We're a place where coders share, stay up-to-date and grow their careers.

Log in Create account

[8]ページ先頭

©2009-2025 Movatter.jp