Skip to content

HelloWorld retains too much code with build optimizer #21880

Closed

Description

@mhevery

We can create hello world in with Ivy in both closure and rollup with build optimizer.

  • Closure: 8,152 bytes
  • Rollup with BO: 34,304 bytes

So it seems that BO is still retaining about 25kb of unneeded code. Need to investigate

rm -rf dist
./built.sh
cd integration/hello_world__render3__closure
yarn install
yarn test
ls -al dist/bundle.js
>>> -rw-r--r--  1 misko  eng  8152 Jan 29 15:44 dist/bundle.js

cd ../hello_world__render3__rollup/
yarn install
yarn test
ls -al dist/bundle.js
-rw-r--r--  1 misko  eng  34304 Jan 29 16:05 dist/bundle.js

Activity

  1. filipesilva commented on Jan 30, 2018

    @filipesilva
    Contributor

    Looking into this, there is one important Uglify option we set in the CLI that is not set here: compress.pure_getters = true.

    With it the rollup bundle goes down to 16k.

        uglify({
          mangle: true,
          compress: {
            pure_getters: true,
            global_defs: {
              'ngDevMode': false,
            }
          }
        })
    
    $ yarn rollup && ls -lha dist/bundle.js
    yarn run v1.3.2
    $ tsc && rollup -c rollup.config.js
    
    built/index.js → dist/bundle.js...
    (!) Missing exports
    https://github.com/rollup/rollup/wiki/Troubleshooting#name-is-not-exported-by-module
     commonjs-proxy:D:\work\angular\integration\hello_world__render3__rollup\node_modules\tslib\tslib.es6.js
    default is not exported by node_modules\tslib\tslib.es6.js
    1: import * as tslib_es6 from "D:\\work\\angular\\integration\\hello_world__render3__rollup\\node_modules\\tslib\\tslib.es6.js"; export default ( tslib_es6 && tslib_es6['default'] ) || tslib_
    es6;
                                                                                                                                                                             ^
    created dist/bundle.js in 6.9s
    Done in 11.11s.
    -rw-r--r-- 1 kamik 197609 16K Jan 30 11:41 dist/bundle.js
    

    Looking further into it.

  2. filipesilva commented on Jan 30, 2018

    @filipesilva
    Contributor

    There was also a matter of order of plugins. Rollup was showing this warning:

    (!) Missing exports
    https://github.com/rollup/rollup/wiki/Troubleshooting#name-is-not-exported-by-module
     commonjs-proxy:D:\work\angular\integration\hello_world__render3__rollup\node_modules\tslib\tslib.es6.js
    default is not exported by node_modules\tslib\tslib.es6.js
    1: import * as tslib_es6 from "D:\\work\\angular\\integration\\hello_world__render3__rollup\\node_modules\\tslib\\tslib.es6.js"; export default ( tslib_es6 && tslib_es6['default'] ) || tslib_
    es6;
    

    This happened due to Build Optimizer trying to alter the tslib imports I believe. Having it before the other plugins caused the whole of tslib to be retained.

    Moving it to just before Uglify drops the size further down to 12k:

      plugins: [
        nodeResolve({jsnext: true, module: true}),
        commonjs({
          include: 'node_modules/rxjs/**'
        }),
        buildOptimizer(),
        uglify({
          mangle: true,
          compress: {
            pure_getters: true,
            global_defs: {
              'ngDevMode': false,
            }
          }
        })
      ],
    
    $ yarn rollup && ls -lha dist/bundle.js
    yarn run v1.3.2
    $ tsc && rollup -c rollup.config.js
    
    built/index.js → dist/bundle.js...
    created dist/bundle.js in 7s
    Done in 10.74s.
    -rw-r--r-- 1 kamik 197609 12K Jan 30 12:10 dist/bundle.js
    
  3. filipesilva commented on Jan 30, 2018

    @filipesilva
    Contributor

    I know resolution of rxjs modules is different between rollup and closure, so I tried to force usage of files in rxjs/_esm5.

    https://github.com/ReactiveX/rxjs/blob/master/doc/pipeable-operators.md#build-and-treeshaking mentions a way to do path mapping for Webpack, but I couldn't find any for rollup. I looked around a bit and found https://github.com/chjtx/rollup-plugin-paths, which allows path mapping in Rollup.

    Using path mapping the bundle for drops to 10kb:

    import buildOptimizer from 'rollup-plugin-angular-optimizer'
    import nodeResolve from 'rollup-plugin-node-resolve';
    import uglify from 'rollup-plugin-uglify';
    import paths from 'rollup-plugin-paths';
    import pathMapping from 'rxjs/_esm5/path-mapping';
    
    export default {
      input: `built/index.js`,
      output: {
        name: 'hw',
        file: `dist/bundle.js`,
        format: 'iife',
        sourcemap: true
      },
      plugins: [
        paths(pathMapping()),
        nodeResolve({jsnext: true, module: true}),
        buildOptimizer(),
        uglify({
          mangle: true,
          compress: {
            pure_getters: true,
            global_defs: {
              'ngDevMode': false,
            }
          }
        })
      ],
      external: []
    }
    
    $ yarn rollup && ls -lha dist/bundle.js
    yarn run v1.3.2
    $ tsc && rollup -c rollup.config.js
    
    built/index.js → dist/bundle.js...
    created dist/bundle.js in 6.5s
    Done in 10.55s.
    -rw-r--r-- 1 kamik 197609 10K Jan 30 12:54 dist/bundle.js
    
  4. filipesilva commented on Jan 30, 2018

    @filipesilva
    Contributor

    Some time ago I got a very insightful suggestion from @kzc about how using the uglify compress passes: 3 option can help in some code bases when using PURE comments (webpack/webpack#2899 (comment)).

    Applying it in this example further shaves .5kb, resulting in a 9.5k bundle:

          compress: {
            pure_getters: true,
            passes: 3,
            global_defs: {
              'ngDevMode': false,
            }
          }
    
    $ yarn rollup && ls -lha dist/bundle.js
    yarn run v1.3.2
    $ tsc && rollup -c rollup.config.js
    
    built/index.js → dist/bundle.js...
    created dist/bundle.js in 6.7s
    Done in 10.37s.
    -rw-r--r-- 1 kamik 197609 9.5K Jan 30 13:22 dist/bundle.js
    
  5. filipesilva commented on Jan 30, 2018

    @filipesilva
    Contributor

    For reference, the closure example in my system results in a 9.7k bundle instead of 8.1k you see. I do not know why closure performs differently for me.

    But within the same environment there seems to be parity in final size, with rollup+uglify performing slightly better (9.5k vs 9.7k).

  6. kzc commented on Jan 30, 2018

    @kzc

    Also try the unsafe compress option. It is safe for sane JS that does not alter built-in functions and classes. Uglify has to be overly conservative by default.

    $ echo 'console.log("foobar".substr(1,4));' | uglifyjs -c
    console.log("foobar".substr(1,4));
    
    $ echo 'console.log("foobar".substr(1,4));' | uglifyjs -c unsafe
    console.log("ooba");
    

    You might also try disabling keep_fargs to remove unused function parameters. It's fine for code that does not interrogate the Function's length to get the number of args:

    $ echo 'function f(a, b, c){ console.log(a); }' | uglifyjs -c
    function f(a,b,c){console.log(a)}
    
    $ echo 'function f(a, b, c){ console.log(a); }' | uglifyjs -c keep_fargs=false
    function f(a){console.log(a)}
    

    If you post the uglify input file in a gist or equivalent perhaps I could offer other suggestions. I don't want to build the code.

  7. filipesilva commented on Jan 30, 2018

    @filipesilva
    Contributor

    @kzc thanks for chiming in with some more suggestions!

    For reference, here are the relevant files so far:

    Following your suggestions, unsafe: true, brought it down to 9.4k, while keep_fargs: false shaved off some 20 bytes. So final remained 9.4k. Not a big difference, but a difference nonetheless. For general purpose uglification I'd probably leave them off.

  8. kzc commented on Jan 30, 2018

    @kzc

    @filipesilva What you have is more or less optimal as far as uglify-es settings go.

    You could save an additional 200 bytes by using rollup-plugin-uglify@2.0.1 which uses ES5-only uglify-js@3 instead of rollup-plugin-uglify@3 which uses ES6+ uglify-es@3. The difference is due to the fact that uglify-js has a rename pass that uglify-es lacks due to ES6+ implementation issues. The rename pass runs before compress and assigns every symbol a globally unique name which facilitates better function inlining.

  9. filipesilva commented on Jan 30, 2018

    @filipesilva
    Contributor

    @kzc thanks for checking these results with me, it really helps to have the validation 👍

    The current results I got were with rollup-plugin-uglify@2.0.1 actually. I tried 3.0.0 and exactly like you said there was roughly 200bytes extra.

  10. kzc commented on Jan 30, 2018

    @kzc

    Rollup has its own equivalent of pure_getters called treeshake.propertyReadSideEffects: false. Perhaps if it is set in the rollup phase prior to uglify it may result in a smaller bundle.

  11. filipesilva commented on Jan 30, 2018

    @filipesilva
    Contributor

    I wasn't aware of those options actually! Just tested them now, and neither pureExternalModules nor propertyReadSideEffects (nor both) reduce the output beyond the 9.5k threshold.

    Also tried turning on pureExternalModules: true to see if it could do without Build Optimizer but it generated a 124k bundle instead.

  12. kzc commented on Jan 30, 2018

    @kzc

    Oh well - worth a shot.

    If you study the uglify output and identify any low hanging fruit micro-optimizations that could be implemented, by all means create a new uglify issue detailing the optimization heuristic.

  13. kzc commented on Jan 30, 2018

    @kzc

    Just curious - what was the total build time for closure's 8.1K (9.7k?) bundle compared to rollup+BO+uglify's 9.5K bundle?

  14. filipesilva commented on Jan 30, 2018

    @filipesilva
    Contributor

    Here are the times on my machine:

    $ time yarn rollup
    yarn run v1.3.2
    $ rollup -c rollup.config.js
    
    built/index.js → dist/bundle.js...
    created dist/bundle.js in 6.7s
    Done in 8.07s.
    
    real    0m9.154s
    user    0m0.060s
    sys     0m0.136s
    
    $ time yarn closure
    yarn run v1.3.2
    $ java -jar node_modules/google-closure-compiler/compiler.jar --flagfile closure.conf
    Done in 4.92s.
    
    real    0m5.958s
    user    0m0.061s
    sys     0m0.075s
    

    So rollup was 9s while closure was 6s.

    I did have a look at the 9.7k closure bundle and something I noticed was property renaming. I think closure does that really well especially with the extra info in comments that angular packages have.

    But I didn't look too deeply because those missing 1.6k still make me wonder a bit. I want to figure out if it's an env thing before I start looking too deeply into the final bundle code.

    Also need to check with @mhevery if I can configure closure to do the equivalent of uglify mangle: false, output: { beautify: true },, which makes reading the final bundles much nicer.

  15. kzc commented on Jan 30, 2018

    @kzc

    Closure's java build time is very good. Much better than I remember. Is google-closure-compiler-js comparable for those who want to avoid installing the JRE?

    As far as property mangling goes, uglify also supports this:

    https://github.com/mishoo/UglifyJS2#cli-mangling-property-names---mangle-props
    https://github.com/mishoo/UglifyJS2#mangle-properties-options

    It can reduce minified output by 40% in some cases. It optionally supports property whitelists and regex patterns as well as quoted property exclusion. Uglify property mangling is discouraged for general use but is fine for bundling a specific library or application where source rules governing properties are strictly enforced.

  16. mhevery commented on Jan 30, 2018

    @mhevery
    ContributorAuthor

    @filipesilva this is awesome investigation. You rock! Let me us internalize this and we will get back to you. No more action is needed from your side for now.

  17. alexeagle commented on Jan 30, 2018

    @alexeagle
    Contributor

    @kzc unfortunately the JS version of closure compiler uses Google Web Toolkit to transpile from Java, and it's bad. The result consumes a lot of memory, and doesn't have feature parity. For example, it doesn't support unicode so no Angular app could ever use it (we use the lower barred latin o character for "private namespacing")
    The right sol'n is just to statically link an openjdk into the regular NPM distro of closure compiler so you aren't aware of the Java runtime dependency. I tried to tell them but sadly not everyone listens to me.

  18. kzc commented on Jan 31, 2018

    @kzc

    @alexeagle Thanks for pointing out the difference between the two Closure versions.

    @filipesilva FWIW, here's the size of the uglified bundle with property mangling:

    $ bin/uglifyjs renderer3-hello-world.js --toplevel -mc pure_getters,passes=3,unsafe -d ngDevMode=false --mangle-props keep_quoted | wc -c
        7563
    

    Whether it works in this case without a mangle property whitelist is another matter - probably it wouldn't. The command above excludes mangling built-in DOM props as well as any property that is quoted at least once in the input (i.e.: ({"foo": 1}) or obj["bar"]) - even in dead code.

  19. alexeagle commented on Jan 31, 2018

    @alexeagle
    Contributor

    Hey @filipesilva
    I have this somewhat in a real state, close to being ready to land in Angular:

    https://github.com/alexeagle/angular/tree/build-optimizer

    Just run
    $ bazel build packages/core/test/bundle/hello_world:all

    in dist/bin/packages/core/test/bundle/hello_world/ you'll find files that trace what happened:

    _bundle.rollup.conf.js was the config passed to rollup --config. Rollup receives the application and rxjs as ES2015, and gets Angular with the rollup-plugin-node-resolve plugin which means we get ES5ESM. You can see that build-optimizer is included in here.

    bundle.es6.js is the output from rollup

    then we run it through tsc to downlevel, and get bundle.js

    then we run it through purify, to get _bundle.post-purify.js

    then we run it through uglify.
    _bundle.uglify.json was the config passed to uglify --config-file
    bundle.min.js is the final result.

    Also, if you request the debug output:
    $ bazel build packages/core/test/bundle/hello_world:bundle.min_debug.js

    it will use _bundle.debug.uglify.json and produce bundle.min_debug.js which is human-inspectable.

    I'm still playing with options to get the small sizes you got - but we don't have a test here that the application still works, and I'd rather know when the options are too aggressive.

  20. filipesilva commented on Jan 31, 2018

    @filipesilva
    Contributor

    @kzc going down to 7.5k with property mangling is pretty impressive. I tried it once yesterday but for some reason didn't get much size reduction. I probably messed up the config at the time.

    @alexeagle you mention that you run tsc from bundle.es6.js to get the ES5 bundle.js. But remember that TS is stripping the PURE comments (see angular/devkit#412). Also Build Optimizer might not detect some of the ES2015 input, the most aggressive optimizations expect ES5 patterns.

    Purify is also not needed, as it only does anything at all to webpack bundles.

  21. alexeagle commented on Feb 1, 2018

    @alexeagle
    Contributor

    Okay, I updated the branch above to use ES5ESM inputs from the app instead. It's still not getting the same size, maybe because of these errors:

    angular/packages/core/test/bundle/hello_world/index.js → bazel-out/k8-fastbuild/bin/packages/core/test/bundle/hello_world/bundle.js...
    (!) `this` has been rewritten to `undefined`
    https://github.com/rollup/rollup/wiki/Troubleshooting#this-is-undefined
    ../../../../execroot/angular/bazel-out/k8-fastbuild/bin/packages/core/core.es5_esm/angular/packages/core/src/metadata/di.js
     6:  * found in the LICENSE file at https://angular.io/license
     7:  */
     8: var __assign = (this && this.__assign) || Object.assign || function (t) {
                        ^
     9:     for (var s, i = 1, n = arguments.length; i < n; i++) {
    10:         s = arguments[i];
    ...and 1 other occurrence
    

    note in our case, we are bundling from Angular sources, not from Angular's NPM distribution, because this is a functional test within the angular repo. That's the obvious difference from your repro.

  22. IgorMinar commented on Feb 9, 2018

    @IgorMinar
    Contributor

    is this still valid?

  23. alexeagle commented on Feb 9, 2018

    @alexeagle
    Contributor

    We solved it, buildsize is keeping us honest at 8k

  24. angular-automatic-lock-bot commented on Sep 13, 2019

    @angular-automatic-lock-bot

    This issue has been automatically locked due to inactivity.
    Please file a new issue if you are encountering a similar or related problem.

    Read more about our automatic conversation locking policy.

    This action has been performed automatically by a bot.

  25. locked and limited conversation to collaborators on Sep 13, 2019
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

area: build & ciRelated the build and CI infrastructure of the project

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions