Webpacker: Support dual ES2015+ES5 output

Created on 3 Oct 2017  ·  15Comments  ·  Source: rails/webpacker

I do not know if this is something suitable for inclusion in Webpacker, but I find it interesting.
With JS module support in modern browsers, there is now a way to no longer load polyfills and transpiled ES5 code in those browsers, but ES2015-transpiled code.

See how it works here: https://philipwalton.com/articles/deploying-es2015-code-in-production-today/

In the blog post example, both JS size and execution time have been more than halfed by doing this.

It works by having Webpack compile 2 versions of each pack, each with a different browser target, so Webpacker seems a good place to do it (and output the correct HTML tag).

What do you think about it @javan @dhh @gauravtiwari ?

enhancement

Most helpful comment

It would be great to have an official example/approach documented somewhere, for this behaviour.

All 15 comments

I like this a lot.

On Tue, Oct 3, 2017 at 7:52 AM, Renaud Chaput notifications@github.com
wrote:

I do not know if this is something suitable for inclusion in Webpacker,
but I find it interesting.
With JS module support in modern browsers, there is now a way to no longer
load polyfills and transpiled ES5 code in those browsers, but
ES2015-transpiled code.

See how it works here: https://philipwalton.com/articles/deploying-es2015-
code-in-production-today/

In the blog post example, both JS size and execution time have been more
than halfed by doing this.

It works by having Webpack compile 2 versions of each pack, each with a
different browser target, so Webpacker seems a good place to do it (and
output the correct HTML tag).

What do you think about it @javan https://github.com/javan @dhh
https://github.com/dhh @gauravtiwari https://github.com/gauravtiwari ?


You are receiving this because you were mentioned.
Reply to this email directly, view it on GitHub
https://github.com/rails/webpacker/issues/887, or mute the thread
https://github.com/notifications/unsubscribe-auth/AAAKtXZ66eofmin5ijOQhb15A238bRz5ks5soi3wgaJpZM4PsD5t
.

Ditto :)

Yeah, me too 👍

Although, I am bit unsure about adding this to Webpacker and worried that this will make the setup complicated - 2 manifest.json, 2 babelrc and then some code to sniff multiple browsers and link correct pack tag - too much responsibility 😄 .

I agree that we need to be careful to not add too much complexity here, but I think that overall it is a huge gain in perceived loading time for people using modern browsers (smaller JS files => less to download and parse, + leveraging the VM optimisations allowed by modern code).

There is no browser sniffing here, we just need to specify a custom browserlist with the browsers supporting modules, and babel-preset-env will do its magic and only transpile what needs to be to support those browsers. These minimum browser versions are fixed (see the article).

I think a plan could be:

  • Add a config option to enable this
  • Change the babel-loader config to force babel-env's browserlist setting if we are doing a "modern" build
  • Change the output path for "modern" JS files to something like packs/with_modules
  • Set an env var throught EnvironmentPlugin so we can have code depending on if we are building for a modern browser or not. Example: not loading a fetch() polyfill in that case, as all modern browsers implement it.
  • Have a manifest-modern.json file for this build. It should contain exactly the same assets as the normal build, except for JS files (which would be in packs/with_modules
  • When running webpack, do it twice, the second one with the modern config enabled
  • If the config is set, javascript_pack_tag will generate 2 <script> tags, one using the paths from manifest.json, and with type="module" using the paths from manifest-modern.json
  • Nothing, we are already done 🙂

This is not a lot of code, much of the complexity comes from understanding what happens and what is described in the article I linked above.

What do you feel about this? Did I miss anything?

Obviously this will not help with external modules, as all of those are transpiled to ES5 before being uploaded to NPM. But at least it helps with your app's code.

Some numbers for my small (~5k lines of CS/JS) app: I went from 1.1MB (270kB gzipped) to 913kB (248 kB gzipped).
This does not look like much, but most of my pack files are using big external modules like Immutable, React and Slate, which are (for now) already transpiled and does not benefit from these optimisations.

To get these results, I used for both builds:

  • Babel 7
  • the new useBuiltIns: usage option that only imports the core-js polyfills you need.
  • It also uses the latest uglifyjs-webpack-plugin using the new uglify-es uglifier, to be able to cope with ES2015 syntax. In production.js:
const UglifyJsPlugin = require('uglifyjs-webpack-plugin')
environment.plugins.set('UglifyJs', new UglifyJsPlugin({ sourceMap: true }))
  • whatwg-fetch polyfill disabled in the modern build
  • the following browserlist: chrome >= 60, safari >= 11, ios >= 10.3, firefox >= 54, edge >= 15

I am interested into seeing the numbers for a bigger codebase. Feel free to ping me if you need help to do so, its quite easy to test (Babel 7 is not needed).

For those interested, I started https://github.com/renchap/modern-js-in-browsers to cover the topic of running modern code in recent browsers while maintaining compatibility.
It is a wide and complex topic (especially when you think post-ES6 or when you want to correctly transpile your deps).
I still plan to implement what I described above in Webpacker, but I am waiting for Babel 7 as it brings a lot of improvements to the process.

So for doing this out of Rails environment I used https://github.com/PolymerX/polymer-skeleton as an example. Maybe it will help enabling it in webpacker :)

Simplest approach would be to run webpack on every configuration file using convention like:
development.*.js where * could be anything, in this case module and nomodule. Not sure its that simple to implement in webpacker.

I am still thinking about implementing a dual module / nomodule output when I have some time.

If you are interested in this, some people from Chrome's team are working on a proposal to the HTML spec adding the ability to load different bundles depending on browser's features here: https://github.com/whatwg/html/issues/4432

Take a look at https://github.com/thekashey/devolution

  • compile bundle to esmodules target. That's basically "top line", acceptable by any browser, which supports _"modules"_.
  • compile the bundle (not the original sources) down to IE11 level, adding all required polyfills
  • adds information about bundle mode into the bundle itself, letting you specify publicPath on the fly.

It's quite simple, but quite reliable.

Hi @renchap! Have you done any further testing (or a proof of concept) for this approach with Webpacker? I'm looking to implement the module/nomodule pattern as well.

I do not have time to work on this unfortunately.
I think the best way to implement it is to have a config entry in webpacker.yml so you can choose to enable it for production only (can't work on dev with WDS, and not really useful for tests).

Then if enabled you need to run webpack twice, with and without an env variable that enables target: "esmodules" in babel config and chancing the output path.

Then you need to handle both manifests (probably merging them), and patch the view helpers to output the 2 <script> tags.

Thanks for the quick follow up @renchap ! I think I got most of that, but wanted to follow up to ensure I'm on the right track (mainly surrounding the webpacker.yml piece)

webpacker.yml

default: &default
  public_output_path: assets/modern
  # ... more config values

# esm build
production: &production
  <<: *default
  # ... more config values

# build for legacy (nomodule) browsers
legacy:
    <<: *production

    public_output_path: assets/legacy

babel.config.js

...
[
  require('@babel/preset-env').default,
  {
    targets: {
      "esmodules": process.env.RAILS_ENV !== 'legacy'
    }
  },
]
...

Run webpack for ESM and non-ESM builds:

  • RAILS_ENV=production bundle exec rails assets:precompile
  • RAILS_ENV=legacy bundle exec rails assets:precompile

(the manifest/view helper advice I understand but it will take a bit more time to implement)

I would not go with 2 different environments in webpacker.yml, but with something like this:

production: &production
  <<: *default
  enable_modules_output: true # esm build enabled, probably needs a better name
  # ... more config values

And then this set an environment variable during the build, and changes the webpack configuration to output to 2 directories and run webpack twice.

People do not need to build things twice or get more control over this, just enable the option and all works.

Do either of you have a working configuration for this that I could look at? Thank you!

@noelherrick I have not had time to circle back to work on this. If you end up coming up with something definitely let me know!

It would be great to have an official example/approach documented somewhere, for this behaviour.

Was this page helpful?
0 / 5 - 0 ratings

Related issues

ytbryan picture ytbryan  ·  3Comments

amandapouget picture amandapouget  ·  3Comments

ijdickinson picture ijdickinson  ·  3Comments

eriknygren picture eriknygren  ·  3Comments

towry picture towry  ·  3Comments