Jahed Ahmed Software, Games

Babel and UglifyJS Pains

Recently, I've been thinking about browser support for FrontierNav. Most browsers support native ECMAScript 2015 (ES6) code natively, but somewhat popular yet unmaintained browsers like Android 4.4, Internet Explorer 11 and Chrome 29 have been holding things back.

So, I decided to drop support for them and go full ES6 in production. I thought the ecosystem and build tools were ready. I was wrong.

babel-preset-env

Babel's recommended way to support ES6 or newer syntax in code is to use babel-preset-env. You can configure this library to target a range of browsers and it will automatically apply the correct Babel Presets, Plugins and Polyfills for any ES6+ syntax those browsers don't support. By doing this, you reduce the amount of code transformations and general bloat.

The browser support is chosen using browserslist, a common utility also used by other compatibility tools like autoprefixer for CSS.

Before making any changes, I was using the defaults, which is roughly the last 2 versions of any known browser and any browser used by more than 0.5% of the tracked population.

1
> 0.5%, last 2 versions, Firefox ESR, not dead

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
and_chr 66
and_ff 57
and_qq 1.2
and_uc 11.8
android 62
android 4.4.3-4.4.4
android 4.4
baidu 7.12
chrome 66
chrome 65
chrome 64
chrome 63
chrome 49
edge 17
edge 16
firefox 59
firefox 58
firefox 52
ie 11
ie_mob 11
ios_saf 11.3
ios_saf 11.0-11.2
ios_saf 10.3
op_mini all
op_mob 37
opera 51
opera 50
opera 49
safari 11.1
safari 11
samsung 6.2
samsung 5
samsung 4

It's worth noting that the tools that use browserslist can only use a subset of this information. babel-preset-env only checks for Chrome, Firefox, Safari, Android, Edge and Internet Explorer versions. From those, I found that:

  • Chrome 29 is still used despite being ancient. This is because it's the first version for Android and no one updates their stuff when given a choice.
  • IE 11 is dead weight at this point.
  • Android 4.4's browser is the last Android-specific browser. All recent versions have shipped with Chrome or a vendor-specific one like Samsung Browser.

These 3 browsers together are holding back babel-preset-env from dropping the vast majority of ES6 transformations. Things like const and arrow functions. Since they were also barely a blip in my audience, I decided to get rid of them using this browserslist:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
# Popularity
> 0.5%

# Limit to recent versions
not chrome < 60
not and_chr < 60
not firefox < 50
not and_ff < 50
not safari < 10
not ios_saf < 10
not opera < 50
not samsung < 5

# Drop support
not op_mini all
not and_uc > 0
not ie > 0
not android > 0

Simple right? Wrong.

Version Confusion

The current stable version of babel-preset-env is v1, which does not support the common ways of sharing browserslist configuration across tools.

I didn't realise this until enabling the debug and noticing the mismatch between that browserslist says and what babel-preset-env says.

Turns out, I was reading the wrong documentation! The README on GitHub is for v7, the real documentation is on the Babel website. I wish this was made obvious so that I could save an hour.

The real configuration is in the .babelrc:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
{
"presets": [
[
"env",
{
"targets": {
"browsers": [
// copy/paste browserslist here
]
},
"useBuiltIns": true
}
]
]
}

Does it work?

Yes! ... Does it work in production? No.

uglify-es

Now that the bundled code has ES6 syntax, we need to use uglify-es. A version of UglifyJS which supports such syntax. Now comes the next problem.

With uglify-es you need to specify your target version. What is our target version? Well, I don't know. The whole point of babel-preset-env is to support any ES6+ syntax on any given environment. It abstracts away the need to know what "version" is going out.

By default uglify-es targets an ES5 environment. So I gave it a shot. But I started getting all of these errors in Rollbar:

1
TypeError: invalid assignment to const `t'

1
TypeError: Attempted to assign to readonly property.

Looking at the output code, it's obvious that uglify-es is trying to optimise code and causing naming conflicts across various scopes. Unlike var, const doesn't like that.

I tried targeting ES6, I tried disabling various configurations, all with limited results. There are few issues filed related to this, it's all very complicated. Doesn't help that uglify-es, despite being different from UglifyJS, is a branch and shares the same repo and issues tracker. To summarise it's organised:

  • UglifyJS is the old UglifyJS
  • UglifyJS2 is the new UglifyJS in the UglifyJS2 v2.x branch.
  • UglifyJS3 is the new UglifyJS2 in the UglifyJS2 master branch.
  • uglify-es is an alternative ES6+ aware UglifyJS3 in the UglifyJS2 harmony branch.

After a few hours, I decided to stop. What's the point? Yes, I can ship ES6+ code by doing this, but my deployment is only 100KB smaller and I'll be supporting less browsers. The amount of effort is not worth it.

What next?

Now I've rolled back, everything's back to what it was. A huge waste of time. At least I learnt something. The ES6+ ecosystem still has a long way to go. Everything's complicated, tooling goes out of alignment, documentation is all over the place and there's constant regressions.

I'll keep things as they are for now. I was thinking of using babel-minify instead of uglify-es since it's meant to be "ES6+ aware" too. But it looks kind of slow, the Webpack plugin doesn't look active so it might not work and I'm kind of burnt out from all of this anyway.