angular-cli: bug: all RxJS operators pulled into prod bundle when using lettable imports from 'rxjs/operators'

Versions

Angular CLI: 1.6.3
Node: 8.9.0
OS: darwin x64
Angular: 5.1.2
... animations, common, compiler, compiler-cli, core, forms
... http, language-service, platform-browser
... platform-browser-dynamic, platform-server, router
... service-worker

@angular/cdk: 5.0.2
@angular/cli: 1.6.3
@angular/flex-layout: 2.0.0-beta.12
@angular/material: 5.0.2
@angular-devkit/build-optimizer: 0.0.36
@angular-devkit/core: 0.0.22
@angular-devkit/schematics: 0.0.42
@ngtools/json-schema: 1.1.0
@ngtools/webpack: 1.9.3
@schematics/angular: 0.1.11
@schematics/schematics: 0.0.11
typescript: 2.5.3
webpack: 3.10.0

Repro steps

https://github.com/Toxicable/issue-lettable-bundle-size

Observed behavior

From https://github.com/angular/angular-cli/issues/8854#issuecomment-354532989, "There appears to be a bug in Webpack that is causing the CLI to import all of the RXJS operators if you use any lettable operator imports without doing each as a deep import individually.

Upgrading to lettable operators in CLI 1.6.3 caused RxJS to go from 60 KB to 122 KB for me. It sounds like a fix is ‘coming soon’, but I haven’t been able to track down the issue to follow."

Another user mentioned a bundle size increase when moving to lettable operators in the CLI: https://github.com/angular/angular-cli/issues/8720#issuecomment-348804558

Desired behavior

From the 5.0 blog post: “These new operators eliminate the side effects and the code splitting / tree shaking problems that existed with the previous ‘patch’ method of importing operators.” “RxJS now distributes a version using ECMAScript Modules. The new Angular CLI will pull in this version by default, saving considerably on bundle size.”

I desire a considerable reduction in bundle size, not an increase.

Mention any other details that might be useful (optional)

I tracked this down across Slacks, GitHub, and Twitter as I was told that there was a known Webpack problem that would be fixed soon and then pulled into the CLI. As far as I can tell, any similar Webpack issue was solved in 3.9.0 and the current CLI already includes it as it is using 3.10.0 now.

I can provide source map explorer screenshots if needed, but the linked repo above should enable you to better reproduce and create your own source map explorer output.

RxJS Docs for lettable operators and builds: https://github.com/ReactiveX/rxjs/blob/master/doc/lettable-operators.md#build-and-treeshaking

Workaround

Breaking out all of your imports into individual (one import per line) deep imports seems to avoid this issue, but it is very painful and not desirable to have these extra lines of imports in projects of any significant size or scope.

About this issue

  • Original URL
  • State: closed
  • Created 6 years ago
  • Reactions: 20
  • Comments: 56 (45 by maintainers)

Most upvoted comments

Here is a repo showing RxJs sizes with webpack 3 and 4: https://github.com/filipesilva/webpack-rxjs-operators.

Only the used operators are retained with Webpack 4.


Webpack side-effects flag comparison on RxJs operators

This repo compares how using "sideEffects": false reduces bundle size for RxJs operators.

It includes a lazy chunk to add indirection to module loading. This prevents Webpack 3 with UglifyJS from dropping the unused RxJs imports but Webpack 4 side effect detection can still remove used code.

cd webpack-3
npm i
npm start 
# source-map-explorer will open on your browser, with all rxjs operators in the main bundle
cd ../webpack-4
npm i
npm start
# source-map-explorer will open on your browser, with only the used rxjs operators in the main bundle

Webpack 3 bundle sizes:

-rw-r--r-- 1 kamik 197609  159 Jan  5 16:37 0.js
-rw-r--r-- 1 kamik 197609 125K Jan  5 16:37 main.js

Webpack 4 bundle sizes:

-rw-r--r-- 1 kamik 197609 5.7K Jan  5 16:40 0.js
-rw-r--r-- 1 kamik 197609 8.7K Jan  5 16:40 main.js

License

MIT

@IgorMinar I created a similar repro with a basic Angular app (https://github.com/filipesilva/webpack-angular) and the results weren’t so good. There was no real size difference.

I think this is because Angular 5 uses flat ESM.

According to doc in https://github.com/webpack/webpack/tree/next/examples/side-effects:

The "sideEffects": false flag in big-module’s package.json indicates that the package’s modules have no side effects (on evaluation) and only expose exports. This allows tools like webpack to optimize re-exports. In the case import { a, b } from "big-module-with-flag" is rewritten to import { a } from "big-module-with-flag/a"; import { b } from "big-module-with-flag/b".

Since Angular 5 is using flat ESM, it is not possible to rewrite import { something } from '@angular/core'; to import { something } from '@angular/core/something'; because there is no @angular/core/something dir/file.

We should run some tests with a non-flat ESM build of Angular to see the impact.

@blackholegalaxy @Lakston this the the correct issue to track resolution.

The only way to truly achieve tree shaking of RxJs (in the current context) is to use Webpack 4 and have the package marked as being side effect free.

Without these two preconditions, it is only possible to treeshake RxJs operators in the main bundle, and only if the vendor bundle is disabled (as it is by default on prod builds).

image

I have an Angular app using v6.1.8, CLI v6.2.3 and rxjs v6.3.2. Not importing anything from the internal path and it’s still appearing inside of the bundle, in addition to that I am seeing bunch of operators that we are not using at all.

The problem is that you have -vc turned on. Vendor chunking results terrible size regressions. This is because webpack 3 generates a ton of cross-chunk module-stitching code that prevents uglify from dead-code-eliminating the unused operators. Once you turn off vendor chunking everything will work correctly.

If things go as planned webpack 4 should significantly improve this situation, but until then definitely always always turn off vendor chunking if you care about payload size. This is why -vc is disabled in cli by default.

with vendor chunking: screen shot 2018-01-02 at 2 43 38 pm

without vendor chunking: screen shot 2018-01-02 at 2 43 42 pm

@Splaktar there is no webpack 4 issue specifically for this, it’s an optimization that was baked into the v4 branch already, so if there was an issue, it should be closed now.

I’m also not aware of a particular rxjs issue for turning on the side-effect free flag so I created one: https://github.com/ReactiveX/rxjs/issues/3212

@TheLarkInn I tried with webpack@4.0.0-alpha.3 and it still seemed to happen. I will setup a bare webpack repro tomorrow so we can look at this sort of effect in more detail.

It boils down to this:

  • currently unused side-effect free exports are dropped via uglify (via the PURE comment)
  • uglify can only drop code that is in the same chunk+module (via ModuleConcatenationPlugin)
  • when there are multiple chunks wired via webpack modules, uglify cannot drop these exports

@IgorMinar am I missing something here?

I’ll setup a repro where webpack3+uglify is used on code using imports from rxjs with code splitting. Then I’ll try to use webpack4 and mark rxjs as being side effect free by editing package.json directly.

This should let us see the exports being dropped in webpack 4.

@songawee the issue is really in webpack v3 and the whole bundling stack outside of the cli, so it’s expected that you can reproduce it without the cli.

@filipesilva keep in mind that I don’t expect that webpack v4 will just make things work out of the box. we’ll need to flag the rxjs package as side-effect-free and possibly do even some repackaging of that npm package. all of that is planned for rxjs v6

@Splaktar the current workaround is to use module id rxjs/operators/<operator-name> instead of rxjs/operators to import operators. You definitely want lettable operators even with this flaw because the original operators that you had to bring in via /add/operator/<operator-name> always end up in the main bundle (critical path) even if they are used only in lazy-loaded code, and more importantly are headache due to far-away effects that can make your code work without /add/operator/foo import if you rely on an operator that was brought in by another part of your code or a 3rd party library (if this far-away code changes and removes the operator import, your code will suddenly break and you will wonder why).

Note that some operators come from libraries. So they are still needed in the bundle even if you don’t use them directly.

ng build --target=production -sm Pre-lettable operators (60.63 KB for RxJS): screen shot 2018-01-02 at 6 43 44 pm

ng build --target=production -sm With lettable operators (122.39 KB for RxJS): screen shot 2018-01-02 at 6 45 08 pm

ng build --target=production -sm --vc=false With lettable operators (122.39 KB for RxJS): screen shot 2018-01-02 at 6 49 10 pm

ng build --target=production -sm --vc false With lettable operators (122.39 KB for RxJS): screen shot 2018-01-02 at 6 51 28 pm

ng build --target=production -sm -vc false With lettable operators (122.39 KB for RxJS): screen shot 2018-01-02 at 6 59 47 pm

ng build --target=production -sm -vc=false With lettable operators (122.39 KB for RxJS): screen shot 2018-01-02 at 7 04 04 pm

ng build --target=production -sm -vc=false -cc=false With lettable operators (122.39 KB for RxJS): screen shot 2018-01-02 at 7 20 41 pm

I found this issue because I was also experiencing this issue with a custom Webpack 4 + Babel 6 setup. I then tried Rollup just for fun to see if it makes any difference. Using RxJS and one/two pipeable operators resulted in a ~10 kb bundle with Rollup. Webpack on the other hand created a 120 kb bundle. But I noticed that Rollup requires the following line in .babelrc:

{
  "presets": [
    [
      "env",
      {
        "modules": false // <- this
      }
    ]
  ]
}

After adding this line in the .babelrc file of my Webpack setup as well its created bundle file shrank by ~110 kb to about the same size that Rollup produces.

Now I guess the Angular CLI uses TypeScript instead of Babel. From reading the documentation it seems to have a similar option in tsconfig.json called compilerOptions.module and a matching value of "None".

I didn’t try this (and I don’t have any Angular projects right now) but maybe this helps you finding a solution? But at least to me this also means that this is not necessarily a Webpack issue, right?

It includes a lazy chunk to add indirection to module loading. This prevents Webpack 3 with UglifyJS from dropping the unused RxJs imports but Webpack 4 side effect detection can still remove used code.

@filipesilva 125K vs 8.7K? that’s actually a bit better than what I expected. Does the app still work?

The webpack example demonstrating this feature is here: https://github.com/webpack/webpack/tree/next/examples/side-effects

@TheLarkInn I haven’t tried v4 yet, but I did try and create a minimal webpack repo - https://github.com/songawee/webpack-example. The results are a bit different from what I expected, but the bundle size does grow when not using deep imports. Let me know if you need anything updated in the repo to make it a better example.

If we did want to test v4, would we have to specify RxJS to be side-effect: false?

I’m not sure if this bug is specific to the CLI, but I’ve reproduced the issue of increasing bundle size with RxJS without using the CLI - https://github.com/songawee/angular-example.

Perhaps there’s something more at play here? 🤔

@Splaktar I remember facing this problem some time ago when importing from rxjs/operators, so I switched to using rxjs/operators/<name> imports everywhere and haven’t had problem with RxJS bundle size ever since. It’s not a big deal, since imports are added by IDE automatically and I never have to write them manually anyway.

Maybe one option for you is to advice using deep imports (at least as a temporary solution). I’ve updated your reproduction from https://github.com/angular/angular-cli/issues/9069#issuecomment-354976634 to use this approach and RxJS bundle size is back to normal.