Fixing Overly Aggressive Optimization with Terser

Importing a legitimate library has recently broken our production bundle, by introducing a “Syntax Error” exception. This short article describes the error and the investigation process that led us to the solution. I hope that at some point, someone will be able to save precious time by referring this post.

TL;DR When bundling in production mode with Webpack v4, TerserPlugin minifies the bundle, which can sometimes corrupt an already-minified 3rd party bundle. Adding the option keep_fnames: /./ will solve this problem without increasing the bundle size.

Mysterious Syntax Error

In one of our current projects, we noticed an odd behavior — some of our tests indicated that our bundle was corrupted (by a “Syntax Error”). At first, we thought this was an issue with our test setup, since the stories for our components (using Storybook) worked fine. Then we realized that even the “production” bundle was causing the same error (fortunately, since the tests failed at CI builds, no corrupted bundle got to our production environment).

The error itself:

SyntaxError: Unexpected token (

Caused by the following piece of code:

function(e,t){if(“function”!=typeof t&&null!==t)throw new TypeError(“Super expression must either be null or a function, not “+typeof t);
……

Finding a suspect

The first thing we did was isolating the problem. Soon enough we concluded that importing a specific dependency (FusionCharts, an awesome chart library) — caused the problem. If we did not import FusionCharts, the bundle was not corrupted.

FusionCharts is widely used, and couldn’t possibly cause this error directly by itself. The only thing that was interesting about FusionCharts bundle, is that it was minified. We noticed, for example, that function names were a single-character long.

An example of a single-character-named function:

function c(e,t){if(“function”!=typeof t&&null!==t)throw new TypeError(“Super expression must either be null or a function, not “+typeof t);

Since the code of FusionCharts worked perfectly in Storybook, we concluded that the problem is not with FusionCharts. After all, there is a big FusionCharts community out there — if a basic use of FusionCharts was truly the source of the problem, someone out there would have written about it already.

At this point, we noticed that most of the tests that were failing were running on a storybook-static build. At first, we did not understand what could be so different between a regular run of Storybook and a build of storybook-static.

By digging in Storybook’s code, we found that the static Storybook page is built in “production-mode”. If we forced storybook to run in non-production mode (by using NODE_ENV=’development’), the bundle was no longer corrupted.

Inspecting Webpack configurations

We needed to find the configuration that was causing the problem in production mode. Our Webpack configurations contained some custom loaders and plugins — maybe one of them was causing the problem in a production build.

We started testing with our Webpack configurations: loaders, plugins — you name it. We spared nothing.

But alas, it looked like no custom property in our webpack config was related to this corruption. No matter which property in webpack.conf.js we changed, the problem always persisted. What could possibly cause this problem?

Frustrated, we started digging in Webpack official documentation. We stumbled upon something interesting:

webpack v4+ will minify your code by default in production mode.

Could it be that TerserPlugin (the default minifier in production mode) is causing us this much trouble?

Terser, is that you?

By reading TerserPlugin official documentation, we tried setting keep_fnames to true. Just for fun.

And… hooray! The problem was gone. It looks like TerserPlugin was minifying too aggressively, and reducing functions with 1-character-length-names to 0-character-length-names.

Maintaining the bundle size

It wasn’t good enough, we still wanted our bundle to be minified as much as possible. If we used “keep_fnames: true” then our function names wouldn’t be minified, thus our bundle size would increase.

TerserPlugin accepts regex as a param to keep_fnames. We used the regex “keep_fnames: /./” which means — if the function has a name of 1 character, don’t change its name (what were you planning to do with it anyway?)

This way the problem was solved, while keeping the bundle size to a minimum.

Takeaways

Our takeaway from this experience (besides what we learned of Terser), is that it is crucial to run tests on your production-bundled code: sure, your code can have 100% coverage — but remember that Webpack configurations are also some sort of code, which is usually left untested.

🕵️‍♂️ Psst… If you enjoyed this, follow me on Twitter (@omril321) — I tweet regularly about cool things I learn.

Frontend lover ❤️ Tooling enthusiastic 🛠️ React / TypeScript clean-coder ⚛️ 🧼