How the size of the code depends on the minifier, collector, and language. Unexpected webpack update
My name is Ilya Goldfarb, I am a developer of Yandex interfaces. I am interested in following how the tools for building the frontend are developing, so I try to study the changes in each release of popular solutions.
In anticipation of the fifth version of webpack, I want to talk about its seemingly minor release 4.26.0 of November 19, 2018, where, unexpectedly and without declaring war, the default version of the minifier was changed. It used to be a UglifyJS package, now it uses Terser, a fork of UglifyES, a branch of UglifyJS that can compress both ES5 and ES6 code. Terser appeared when the main maintainer refused to support and develop UglifyES. However, UglifyJS also stopped its development in August 2018, when the last release was released. In a new fork, we fixed some bugs and refactored the code a bit.
The API of these minifiers is compatible, but they produce different compression results. Usually changes of this level occur only in major, not minor, updates. Because of this, many developers may not pay attention to the innovation. Of course, in most cases everything will work, but no one wants to become the one who gets bugs on the production of their project due to the build and minification system.
This whole story prompted me to do a little personal research on compression. Here are the questions I asked:
For the research I've done a small application to React 16, which renders the application on the Vue 2, which renders the application on Angular 7, in which there is a whole one button.
In total, 3 529 695 bytes of non-minified code (720 393 gzip bytes) were released.
I took the last available UglifyJS and coming with the Terser webpack with the ES5 option and used the same compression settings.
Bottom line: UglifyJS compresses better by 3.5% (2.5% gzip).
I measured the performance using standard DevTools Yandex Browser. I loaded the page 12 times and took the value Scripting (runtime of the script), discarding the first three dimensions.
UglifyJS - 221 ms (error 2.8%).
Terser - 226 ms (error 2.7%).
Bottom line: the values are too small for such an error, we can consider them the same. We also conclude that this method is not suitable for measuring load time.
I did not measure and compare the speed of the code, since different code works differently. The developers of each project should independently investigate this issue.
To compare the two versions and focus solely on technology, I took Babel plugins and made four assemblies:
Bottom line: the version compressed by Babel with compilation timecode under esnext weighs 97,238 bytes (8.2%) less. This unexpectedly happened a lot, because the angular is written in TypeScript, and Vue and React in JavaScript Terser, like Uglify, cannot build an unused piece of code delivered from the angular with a web script when building with a webpack. This is a compilation bug for this example. In the assembly on another project, it may not be, and the difference will be much smaller.
It is also seen that the volume of ES6 code is less than ES5 by only 2062 bytes. On the pet project, I got a completely different result: ES6 code is 3-6% more than ES5. This is due to several factors, of which two main:
1. The Babel helper for class inheritance is inserted once and then costs four bytes (e (a, b)), and ES6 uses native inheritance at the cost of 15 bytes (class a extends b).
2. The method of declaring variables. In ES5, these are vars, and they compress perfectly. But in ES6 these are let and const, which preserve the initialization order and are not combined with each other.
Unsafe aggressive minification like forced arrow functions or using the loose setting will help reduce the size of the ES6 code. Be careful and consider the subtleties. For example, in Firefox, arrow functions are four times slower than usual, but in Chromium there is no difference.
Therefore, it is impossible to unequivocally answer the question: the result is highly dependent on the code and the target runtime.
Compare whether it is possible to get a smaller file size if you tweak the settings a little. For example, we indicate that the minification must be repeated five times. By default, it passes only once.
Bottom line: Uglify with fivefold minification is less than Uglify by default by 3.7% (3.4% gzip). Therefore, you must always tighten the compression settings. By the way, fivefold minification does not mean that the assembly will go five times longer. For example, in this test project, a single minification takes 18 seconds, five times - 38, and ten times - 49. I recommend experimentally finding the ideal value for your project, after which the minification will stop and the code will not change. Usually it is from 5 to 10. There are also a bunch of other options: comments: false cuts out all comments about licenses (although this is a legal issue), and hoist_funs: true groups functions in one place, which allows better optimization of vars. Ideally, you need to go over all the settings .
Rollup is an alternative collector with a built-in tree shaking mechanism. For the test, I made a build on Rollup 0.67.4 with the same settings as the webpack.
Bottom line: the result from Rollup and Uglify is 5.6% (3.6% gzip) less.
This happened for several reasons:
1. Webpack contains crutches for borderline cases. For example, this code wraps each function call from another module in Object (). This is done to prevent context transfer for modules without use strict to modules with use strict. Well-written projects without third-party dependencies do not need a wrapper, but sometimes not only well-written code is involved in the assembly. And in this regard, webpack looks more reliable. Rollap, in turn, believes that all modules are ES6 modules, and they are always executed in use strict, so this problem simply does not exist for him.
An important question is how such webpack crutches affect performance. Imagine that we wrote the perfect code that does not need additional wrappers, but still, every function call will go through them. This adds a small performance overhead: approximately one hundredth of a microsecond per function call in Chromium (one tenth in Firefox).
2. The webpack has a small bootstrap that controls the initialization and loading of modules. Rollup does not use wrappers, but simply drops the code of all modules into a single scope. The webpack has a similar optimization, but it does not work with all modules.
I hope that many, after reading the article, will check their build systems and make sure that they use all possible tricks for the best compression. It is quick and easy.
First, set up a bunch of TypeScript and Babel correctly. Let each component of the assembly do its own thing: one checks the types, and the second is responsible for converting to obsolete standards.
Secondly, when using ES5, you can change the minifier back to UglifyJS, but remember that it is no longer supported.
Thirdly, it is preferable to choose Rollup for assembly. However, not in all cases this is possible due to the lack of some plugins. After assembly, do not forget to check the functionality by functional tests. If you do not have them - it's time to start writing them.
In anticipation of the fifth version of webpack, I want to talk about its seemingly minor release 4.26.0 of November 19, 2018, where, unexpectedly and without declaring war, the default version of the minifier was changed. It used to be a UglifyJS package, now it uses Terser, a fork of UglifyES, a branch of UglifyJS that can compress both ES5 and ES6 code. Terser appeared when the main maintainer refused to support and develop UglifyES. However, UglifyJS also stopped its development in August 2018, when the last release was released. In a new fork, we fixed some bugs and refactored the code a bit.
The API of these minifiers is compatible, but they produce different compression results. Usually changes of this level occur only in major, not minor, updates. Because of this, many developers may not pay attention to the innovation. Of course, in most cases everything will work, but no one wants to become the one who gets bugs on the production of their project due to the build and minification system.
This whole story prompted me to do a little personal research on compression. Here are the questions I asked:
- What compresses ES5, TerSer or UglifyJS better?
- What is loading faster: a compressed version of ES5 from Terser or from UglifyJS?
- Which version weighs more: ES5 or ES6? And how does TypeScript affect this?
- Is there a big difference between default settings and manual settings?
- And if not webpack? Who produces a smaller assembly, Rollup or webpack?
For the research I've done a small application to React 16, which renders the application on the Vue 2, which renders the application on Angular 7, in which there is a whole one button.
In total, 3 529 695 bytes of non-minified code (720 393 gzip bytes) were released.
What compresses ES5, TerSer or UglifyJS better?
I took the last available UglifyJS and coming with the Terser webpack with the ES5 option and used the same compression settings.
Size in bytes | Size in bytes (gzip) | |
UglifyJS | 1,050,376 | 285,290 |
Terser | 1,089,282 | 292 678 |
What is loading faster: a compressed version of ES5 from Terser or from UglifyJS?
I measured the performance using standard DevTools Yandex Browser. I loaded the page 12 times and took the value Scripting (runtime of the script), discarding the first three dimensions.
UglifyJS - 221 ms (error 2.8%).
Terser - 226 ms (error 2.7%).
Bottom line: the values are too small for such an error, we can consider them the same. We also conclude that this method is not suitable for measuring load time.
I did not measure and compare the speed of the code, since different code works differently. The developers of each project should independently investigate this issue.
Which version weighs more: ES6 or ES5? And how does TypeScript affect this?
To compare the two versions and focus solely on technology, I took Babel plugins and made four assemblies:
- ES5: all plugins marked as es2016, + plugin for Object.assign + plugins for later versions + experimental plugins, target in tsconfig installed in ES5;
- ES5 (ts esnext): all plugins marked as es2016, + plugin for Object.assign + all plugins for later versions + experimental plugins, target in tsconfig is set to esnext;
- ES6: only plug-ins for es2017 and later + experimental plug-ins, target in tsconfig is set to ES6;
- ES6 (ts esnext): only plugins for es2017 and later + experimental plugins, target in tsconfig is set to esnext.
Size in bytes | Size in bytes (gzip) | |
ES5 | 1 186 520 | 322 071 |
ES5 (ts esnext) | 1,089,282 | 292 678 |
ES6 | 1,087,220 | 292 232 |
ES6 (ts esnext) | 1,087,220 | 292 232 |
It is also seen that the volume of ES6 code is less than ES5 by only 2062 bytes. On the pet project, I got a completely different result: ES6 code is 3-6% more than ES5. This is due to several factors, of which two main:
1. The Babel helper for class inheritance is inserted once and then costs four bytes (e (a, b)), and ES6 uses native inheritance at the cost of 15 bytes (class a extends b).
2. The method of declaring variables. In ES5, these are vars, and they compress perfectly. But in ES6 these are let and const, which preserve the initialization order and are not combined with each other.
Unsafe aggressive minification like forced arrow functions or using the loose setting will help reduce the size of the ES6 code. Be careful and consider the subtleties. For example, in Firefox, arrow functions are four times slower than usual, but in Chromium there is no difference.
Therefore, it is impossible to unequivocally answer the question: the result is highly dependent on the code and the target runtime.
Is there a big difference between default settings and manual settings?
Compare whether it is possible to get a smaller file size if you tweak the settings a little. For example, we indicate that the minification must be repeated five times. By default, it passes only once.
Size in bytes | Size in bytes (gzip) | |
Terser (default) ES5 | 1,097,141 | 294 306 |
Terser (passes 5) ES5 | 1 089 312 | 292,408 |
Uglify (default) ES5 | 1 091 350 | 294,845 |
Uglify (passes 5) ES5 | 1,050,363 | 284 618 |
Who produces a smaller assembly, Rollup or webpack?
Rollup is an alternative collector with a built-in tree shaking mechanism. For the test, I made a build on Rollup 0.67.4 with the same settings as the webpack.
Size in bytes | Size in bytes (gzip) | |
Rollup ES5 (Uglify) | 990 497 | 274 105 |
Rollup ES5 (Terser) | 995 318 | 272 532 |
webpack ES5 (Uglify) | 1,050,363 | 284 618 |
webpack ES5 (Terser) | 1 089 312 | 292,408 |
This happened for several reasons:
1. Webpack contains crutches for borderline cases. For example, this code wraps each function call from another module in Object (). This is done to prevent context transfer for modules without use strict to modules with use strict. Well-written projects without third-party dependencies do not need a wrapper, but sometimes not only well-written code is involved in the assembly. And in this regard, webpack looks more reliable. Rollap, in turn, believes that all modules are ES6 modules, and they are always executed in use strict, so this problem simply does not exist for him.
An important question is how such webpack crutches affect performance. Imagine that we wrote the perfect code that does not need additional wrappers, but still, every function call will go through them. This adds a small performance overhead: approximately one hundredth of a microsecond per function call in Chromium (one tenth in Firefox).
2. The webpack has a small bootstrap that controls the initialization and loading of modules. Rollup does not use wrappers, but simply drops the code of all modules into a single scope. The webpack has a similar optimization, but it does not work with all modules.
Study Summary
I hope that many, after reading the article, will check their build systems and make sure that they use all possible tricks for the best compression. It is quick and easy.
First, set up a bunch of TypeScript and Babel correctly. Let each component of the assembly do its own thing: one checks the types, and the second is responsible for converting to obsolete standards.
Secondly, when using ES5, you can change the minifier back to UglifyJS, but remember that it is no longer supported.
Thirdly, it is preferable to choose Rollup for assembly. However, not in all cases this is possible due to the lack of some plugins. After assembly, do not forget to check the functionality by functional tests. If you do not have them - it's time to start writing them.
Only registered users can participate in the survey. Please come in.
How do you collect your projects
- 8.2% rollup 23
- 86.6% webpack 241
- 16.9% gulp 47
- 3.2% grunt 9
- 3.9% proprietary solution 11