Skip to content

Increased initial main.js bundle size in v9 - mainly due to @angular/material packages #19610

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
1 of 15 tasks
bri1990 opened this issue Feb 7, 2020 · 41 comments
Closed
1 of 15 tasks
Labels
area: build & ci Related the build and CI infrastructure of the project in progress This issue is currently in progress

Comments

@bri1990
Copy link

bri1990 commented Feb 7, 2020

🐞 Bug report

Command (mark with an x)

  • new
  • build
  • serve
  • test
  • e2e
  • generate
  • add
  • update
  • lint
  • xi18n
  • run
  • config
  • help
  • version
  • doc

Is this a regression?

yes,

Description

Our initial bundle increased after the upgrade to angular v9. Below 2 screenshots made with webpack-bundle-analyzer, both are build with the --prod tag on. The complete size of our app decreased in size, but the main bundle increased slightly.

reporting this
@filipesilva who mentioned the following

If you see your bundles increasing in size with Ivy, we want to know. Please open a new issue with a reproduction if possible. If you can’t publicly share your reproduction, consider sharing it privately instead.

In our case the initial bundle from the @angular module in angular 9.0.0 is increased by 170 kb (parsed) compared to the build size in angular 8.3.20

ANGULAR 9.0.0 bundle overview
Screenshot 2020-02-07 at 09 21 38

ANGULAR 8.3.20 bundle overview
Screenshot 2020-02-07 at 09 21 49

🔬 Minimal Reproduction

its a private repo, but can share an bitbucket account via a private message if needed

🔥 Exception or Error

budle size difference

Angular 9:


chunk {33} polyfills-es2015.5bbe6128ce574187632a.js (polyfills) 97.3 kB [initial] [rendered]
chunk {34} polyfills-es5.7489d086305aa2ccf313.js (polyfills-es5) 188 kB [initial] [rendered]
--
chunk {18} runtime-es2015.7b3c2daa7eda967f9d4b.js (runtime) 4.55 kB [entry] [rendered]
chunk {18} runtime-es5.7b3c2daa7eda967f9d4b.js (runtime) 4.54 kB [entry] [rendered]
--
chunk {32} main-es2015.7771c8f8f77604a6a0fb.js (main) 1.13 MB [initial] [rendered]
chunk {32} main-es5.7771c8f8f77604a6a0fb.js (main) 1.32 MB [initial] [rendered]
--
chunk {35} styles.96f30aa09bf98d520a7b.css (styles) 403 kB [initial] [rendered]

Angular 8:


chunk {20} runtime-es2015.8dcc6cc63434c93fc456.js (runtime) 4.68 kB [entry] [rendered]
chunk {20} runtime-es5.8dcc6cc63434c93fc456.js (runtime) 4.67 kB [entry] [rendered]
--
chunk {40} polyfills-es2015.c6262141f4f874e78e85.js (polyfills) 98.1 kB [initial] [rendered]
chunk {41} polyfills-es5.468b140bd31c7a429390.js (polyfills-es5) 185 kB [initial] [rendered]
--
chunk {39} main-es2015.d68e6e1b9429135d5faf.js (main) 1.02 MB [initial] [rendered]
chunk {39} main-es5.d68e6e1b9429135d5faf.js (main) 1.19 MB [initial] [rendered]
--
chunk {42} styles.38056460a1b2b723be01.css (styles) 397 kB [initial] [rendered]

🌍 Your Environment


ngular CLI: 9.0.0
Node: 13.3.0
OS: darwin x64

Angular: 9.0.0
... animations, cdk, cli, common, compiler, compiler-cli, core
... forms, language-service, material, platform-browser
... platform-browser-dynamic, router
Ivy Workspace: Yes

Package                           Version
-----------------------------------------------------------
@angular-devkit/architect         0.900.0
@angular-devkit/build-angular     0.900.0
@angular-devkit/build-optimizer   0.900.0
@angular-devkit/build-webpack     0.900.0
@angular-devkit/core              9.0.0
@angular-devkit/schematics        9.0.0
@angular/flex-layout              9.0.0-beta.28
@ngtools/webpack                  9.0.0
@schematics/angular               9.0.0
@schematics/update                0.900.0
rxjs                              6.5.4
typescript                        3.7.5
webpack                           4.41.5

Anything else relevant?

Similar issues: angular/angular-cli#16989

@filipesilva
Copy link
Contributor

Heya, thanks for the report!

The current situations we know this might happen in are documented in https://angular.io/guide/ivy-compatibility#payload-size-debugging. Can you take a look at case 1. and 2. just in case they apply in your case?

I'd like to take a look at your project if possible. I think the most straightforward way to share it would be a private github repo (these are now free) that you invite me to. Would that work?

@filipesilva filipesilva self-assigned this Feb 7, 2020
@bri1990
Copy link
Author

bri1990 commented Feb 7, 2020

@filipesilva
Ok done, I had a look last night at the debugging post, will have another look later today. In the meantime I gave you access to the private repo

@filipesilva
Copy link
Contributor

@bri1990 looking at the project at the moment. Can you tell me in which of the apps you saw the increased bundle size?

Since you have two apps and many shared libraries, I think you are suffering from case 2 in https://angular.io/guide/ivy-compatibility#payload-size-debugging. Can you try the approach described in angular/angular-cli#16799 (comment) and see if it works please?

@bri1990
Copy link
Author

bri1990 commented Feb 7, 2020

@filipesilva in the edu-app, it uses both the @education as the @banax folder (ng build --prod --project edu-app, we currently only use that one in that repo),

Can you try the approach described in angular/angular-cli#16799 (comment) and see if it works please?
Ok I'm going to have a look. To explain the our repo a little you see multiple libraries but these only contain style sheets and components that are loaded into our main app

Regarding case 2:
I added "sideEffects": false to the main package.json (we only have one in the repo you have access), this resulted in a 0.05mb decrease in our main bundle ++

this gets us very close to the v8 build size

@filipesilva
Copy link
Contributor

It's not a great idea to add it in the main toplevel one, because your apps do have at least side effects in main.ts. It's better to add it in the library roots. If the library doesn't actually have a package.json, you can add one with just {"sideEffects": false}.

@filipesilva
Copy link
Contributor

I tried putting these various package.json and saw a main bundle of 1.09 MB, which is still larger than the 1.02 MB with View Engine. I've got to dig a bit deeper.

@bri1990
Copy link
Author

bri1990 commented Feb 7, 2020

Hi @filipesilva, thanks for the hint! will implement it.

I hope you are able to spot the problem:)

I must admit, I expected angular v9 to reduce the initial load by quite a bit. Are we doing something wrong here? I've seen some examples elsewhere but the initial load remained the same in most cases (examples were done in 9.R.C)

In our case we have quite a large css bundle, I believe 160 kb is for theming alone (dark + normal modus), so some room to improve here.

gr

@bri1990
Copy link
Author

bri1990 commented Feb 11, 2020

@filipesilva using "sideEffects": false is causing some problems for us when serving the angular app after building (via nginx hosting it on google cloud platform). We are still figuring out why this is happening

perhaps it is use full information

@bri1990
Copy link
Author

bri1990 commented Feb 11, 2020

another question is, do you still need access to the repo?

@filipesilva
Copy link
Contributor

@bri1990 are you only seeing the problems when serving on nginx and not on ng serve? If so, it might be a caching issue.

I still need access to the repo because I wanted to find the reason for the size difference. But I also have it on disk from the initial investigation so you can remove my access and I will ask it again if relevant.

@bri1990
Copy link
Author

bri1990 commented Feb 11, 2020

Hi again @filipesilva, no it only happends on nginx, ng serve runs fine. We find it quite hard to debug, so we removed the "sideEffects": false lines for now

I still need access to the repo because I wanted to find the reason for the size difference. But I also have it on disk from the initial investigation so you can remove my access and I will ask it again if relevant.
That is fine! was just wondering, hope you find the problem. I won't remove your access

Will post our findings regarding the nginx problem here

@filipesilva
Copy link
Contributor

Was taking a look today again, and one of the things I tried was to make builds without lazy routes. This way we can see bundle sizes without the de-optimizations that can result with lazy routes.

With Ivy the main bundle was 831296 B. With VE it was 814688 B. So even without lazy routes Ivy makes your bundle larger, which I find surprising.

I made my builds with source maps so I could use source-map-explorer to figure out where the extra size came from. Notably, in Ivy the source maps say 715 KB come from node_modules but in VE it's only 606 KB. Within node_modules, Ivy has 526 KB from @angular/* directories while VE has only 428 KB. And of the angular packages, the Ivy bundle has 126 KB from @angular/material while the VE one only has 55 KB. So looking at @angular/material seemed promising.

The Ivy sourcemaps for material looked like this:
image

And the VE ones:
image

This looks quite odd to me. The Ivy app seems to have a lot more code from Ivy. list.js is especially striking, since it's 29kb in Ivy but only 1kb in VE. I double checked and your app is using the correct deep imports (e.g. @angular/material/list). But I also find it weird that I can't see any .ngfactory.js file in these source maps, and I used to see these before.

I ran ng build edu-app --prod --verbose &> log.txt to see the verbose log for both Ive and VE. The VE log mentions the .ngfactory.js files there for material so at least that confirms they were used.

At this point I feel I'm kinda out of leads so I might as well just go though the verbose log to see if something catches my eyes. I've seen a bunch of these in the past so that's a reasonable enough thing to do.

I did find something odd early on:

 [1hPV] D:/sandbox/banax_angular/node_modules/rxjs/internal/Subscriber.js 9.17 KiB {1} [built]
     ModuleConcatenation bailout: Module is not an ECMAScript module
     cjs require ../Subscriber [JJ8B] D:/sandbox/banax_angular/node_modules/rxjs/internal/util/canReportError.js 3:33-57
     cjs require ../Subscriber [Yfti] D:/sandbox/banax_angular/node_modules/rxjs/internal/util/toSubscriber.js 3:33-57
     cjs require ./Subscriber [d4zx] D:/sandbox/banax_angular/node_modules/rxjs/internal/InnerSubscriber.js 18:33-56
     cjs require ./Subscriber [dmvN] D:/sandbox/banax_angular/node_modules/rxjs/internal/OuterSubscriber.js 18:33-56
     cjs require ../Subscriber [sGav] D:/sandbox/banax_angular/node_modules/rxjs/internal/operators/observeOn.js 18:33-57
     cjs require ./Subscriber [tkgy] D:/sandbox/banax_angular/node_modules/rxjs/internal/Subject.js 19:33-56

This is the entry for the D:/sandbox/banax_angular/node_modules/rxjs/internal/Subscriber.js module. It says why this module is in the compilation, and why it's not concatenated with other modules. Module concatenation is a big part of size savings so it's important to have as little of these ModuleConcatenation bailout as possible.

I saw this ModuleConcatenation bailout: Module is not an ECMAScript module for a bunch of rxjs modules. But since I know that RxJS does ship a ESM version, I find it odd. I searched the project source code for rxjs/ and found a bunch of import { Subject } from 'rxjs/internal/Subject'; and the like.

Projects really shouldn't be importing from rxjs/internal so this looks wrong. I also saw a tslint rule blacklisting rxjs and found that odd, since that is the correct way of importing things like Subject. Maybe at some point we made projects with that lint rule and since then removed it?

I replaced all rx/internal/operators/* imports to rx/operators, and all remaining rx/internal/* ones to just rxjs. I rebuilt and saw the Ivy main down to 799681 B (from 831296) and the VE one down to 783077 B (from 814688). Keep in mind these are still numbers for the app without any lazy routes. So about 31kB down in both. This wasn't the cause of the Ivy vs VE problem but it was something that was going wrong regardless.

@filipesilva
Copy link
Contributor

Still looking at the verbose log, I remove the terser logs at the end and use VSCode's "fold all" functionality to fold log bits. It seems to recognize the boundaries pretty well.

The Ivy listing for modules in main.js looks like this:
image

The Ivy one:
image

Nothing super odd here. A bunch of little modules related to sockets and whatnot, that seem to be commonjs modules, and then a gigantic concatenated main-edu.ts module. They even match line numbers. This looks right.

@filipesilva
Copy link
Contributor

Earlier it occurred to me, and meanwhile I forgot, that that might be unused code in the app folder itself, so I also added {"sideEffects": false} in projects/edu-app/src/app/package.json. It can't be in projects/edu-app/src because main.ts does have side effects. I tried that now, but no change. Still 799681 B.

@bri1990
Copy link
Author

bri1990 commented Feb 12, 2020

@filipesilva are there any reports regarding the addition of {"sideEffects": false}.

We only see some js files (main, polyfills, but not the complete waterfall) served after we build the project and try to serve the compiled bundle (dist) either with nginx or http-server (the node package).

this means that our app gets stuck, we only see the splash screen in our case so we are forced to remove the {"sideEffects": false}

@filipesilva
Copy link
Contributor

@bri1990 can you tell me the paths you added those package.json with {"sideEffects": false}? I can check locally if there's something weird with it.

@bri1990
Copy link
Author

bri1990 commented Feb 12, 2020

we tried multiple ways but they all seem to have the same effect:
initially we tried:
in:
/projects/@banax/package.json
/projects/@education/package.json
/projects/edu-app/package.json

With this code (diff names)
{ "name": "banax-lib", "sideEffects": false }

than we tried to add it only in the main package.json
/package.json

@filipesilva
Copy link
Contributor

filipesilva commented Feb 12, 2020

Ok, some of those are the "wrong" places. "sideEffects": false means "files in this folder have no toplevel side effects". Toplevel code is code that runs when the module is imported, and toplevel side effects are effects other than declarations.

So for instance function foo(){} doesn't really have a side effect, it's just a declaration. But window.foo = function foo(){} is both a declaration and a side effect, because it's changing global state (the properties in window). Having console.log('hello') toplevel is also a side effect.

Essentially you should look at toplevel code and ask "if this code didn't run because I didn't import the file anymore, would things behave differently or break?". If the answer is yes, it's has side effects.

In Angular CLI projects, src/main.ts has two big side effects:

import { enableProdMode } from '@angular/core';
import { platformBrowserDynamic } from '@angular/platform-browser-dynamic';

import { AppModule } from './app/app.module';
import { environment } from './environments/environment';

if (environment.production) {
  enableProdMode();
}

platformBrowserDynamic().bootstrapModule(AppModule)
  .catch(err => console.error(err));

Both enableProdMode() and platformBrowserDynamic().bootstrapModule(AppModule) are side effects. They do things because they were called. If you imported this file from elsewhere, this code would run and do things. If you stopped importing it, these things would not run and the application would not run anymore. So it's not correct to say /src/ or / has no side effects.

In your particular project, these are the places where you should put a package.json containing {"sideEffects": false }:

  • /projects/@banax/, because it is a shared library and you don't want all code to be retained
  • /projects/@education/, for the same reason
  • /projects/edu-app/src/app/ if you feel confident that your app files don't have any side effects it's ok to do this one,

But you can't do /projects/edu-app/ because then it would contain main.ts as well, and that file has side effects.

@filipesilva filipesilva pinned this issue Feb 12, 2020
@filipesilva
Copy link
Contributor

Looked a bit more into Materials List and why it showed such a different size between Ivy and VE. To take a good look at a good I made a debug build using NG_BUILD_DEBUG_OPTIMIZE=minify ng build edu-app --prod. We use this option to get more readable production output and see the symbol names that are in production builds.

I found MatListModule in both Ivy and VE builds and all the other modules that are also listed in node_modules/@angular/material/fesm2015/list.js. I saw that on the Ivy build there seemed to be more code for these classes than in the VE build, even including the VE ngfactory symbols.

From this I was able to make a repro:

git clone https://github.com/filipesilva/ivy-material-list-size
cd ivy-material-list-size
npm install
ng build --prod
# change tsconfig.json to have "enableIvy": false,
ng build --prod

The Ivy build has a main bundle of 232 kB, while the VE build has a main bundle of 221 kb. Still tracking down the reason for this difference in since, but having a minimal example helps a lot.

There might be other things going on besides this though.

@filipesilva
Copy link
Contributor

Ok, I dissected the https://github.com/filipesilva/ivy-material-list-size example and added an explanation. Now that we know what's happening with a bit more detail we'll try to address it.


ivy-material-list-size

This repo shows a size regression with Ivy found in https://github.com/angular/angular-cli/issues/16866.

git clone https://github.com/filipesilva/ivy-material-list-size
cd ivy-material-list-size
yarn
yarn repro

The first build will use Ivy and output to dist/ivy, and the second build will use VE and output to dist/ve.

What's happening?

Given the following template:

<mat-nav-list>
  <mat-list-item>
     hello
  </mat-list-item>
</mat-nav-list>

Ivy produces a 232 kB output while VE produces a 221 kB output, resulting in a 11 kB increase.

But if you change the template to:

<mat-nav-list>
  hello
</mat-nav-list>

Ivy produces a 194 kB output while VE produces a 212 kB output, resulting in a 18 kB decrease.

You can use the command below to produce debug versions of the production bundles.
These debug versions keep the original variable names are are formatted for readability.

NG_BUILD_DEBUG_OPTIMIZE=minify ng build --prod && NG_BUILD_DEBUG_OPTIMIZE=minify ng build --configuration production,ve

The mat-list-ivy.js and mat-list-ve.js files contain contain the code from @angular/material/list that ends up in the production builds, as extracted manually from the debug builds.

The Ivy file retained the following classes:

MatListBase
MatListItemBase
MatNavList
MatList
MatListAvatarCssMatStyler
MatListIconCssMatStyler
MatListItem
MatListModule

The VE file retained the following classes:

MatListBase
MatListItemBase
list_MatNavList
list_MatList
list_MatListItem
MatListModule

Notably, the VE file does not reference either MatListAvatarCssMatStyler or MatListIconCssMatStyler.
In Ivy these two are referenced in the MatListItem factory code:

return MatListItem.\u0275fac = function(t) {
    return new (t || MatListItem)(\u0275\u0275directiveInject(ElementRef), \u0275\u0275directiveInject(ChangeDetectorRef), \u0275\u0275directiveInject(list_MatNavList, 8), \u0275\u0275directiveInject(list_MatList, 8));
}, MatListItem.\u0275cmp = \u0275\u0275defineComponent({
    type: MatListItem,
    selectors: [ [ "mat-list-item" ], [ "a", "mat-list-item", "" ], [ "button", "mat-list-item", "" ] ],
    contentQueries: function(rf, ctx, dirIndex) {
        var _t;
        1 & rf && (\u0275\u0275contentQuery(dirIndex, list_MatListAvatarCssMatStyler, !0),
        \u0275\u0275contentQuery(dirIndex, list_MatListIconCssMatStyler, !0), \u0275\u0275contentQuery(dirIndex, core_MatLine, !0)),
        2 & rf && (\u0275\u0275queryRefresh(_t = \u0275\u0275loadQuery()) && (ctx._avatar = _t.first),
        \u0275\u0275queryRefresh(_t = \u0275\u0275loadQuery()) && (ctx._icon = _t.first),
        \u0275\u0275queryRefresh(_t = \u0275\u0275loadQuery()) && (ctx._lines = _t));
    },
    hostAttrs: [ 1, "mat-list-item" ],
    hostVars: 4,
    hostBindings: function(rf, ctx) {
        2 & rf && \u0275\u0275classProp("mat-list-item-avatar", ctx._avatar || ctx._icon)("mat-list-item-with-avatar", ctx._avatar || ctx._icon);
    },
    inputs: {
        disableRipple: "disableRipple"
    },
    exportAs: [ "matListItem" ],
    features: [ \u0275\u0275InheritDefinitionFeature ],
    ngContentSelectors: list_c2,
    decls: 6,
    vars: 2,
    consts: [ [ 1, "mat-list-item-content" ], [ "mat-ripple", "", 1, "mat-list-item-ripple", 3, "matRippleTrigger", "matRippleDisabled" ], [ 1, "mat-list-text" ] ],
    template: function(rf, ctx) {
        1 & rf && (\u0275\u0275projectionDef(list_c1), \u0275\u0275elementStart(0, "div", 0),
        \u0275\u0275elementStart(1, "div", 1, void 0), \u0275\u0275elementEnd(), \u0275\u0275projection(2),
        \u0275\u0275elementStart(3, "div", 2), \u0275\u0275projection(4, 1), \u0275\u0275elementEnd(),
        \u0275\u0275projection(5, 2), \u0275\u0275elementEnd()), 2 & rf && (1, selectIndexInternal(getTView(), getLView(), getSelectedIndex() + 1, getCheckNoChangesMode()),
        \u0275\u0275property("matRippleTrigger", ctx._getHostElement())("matRippleDisabled", ctx._isRippleDisabled()));
    },
    directives: [ core_MatRipple ],
    encapsulation: 2,
    changeDetection: 0
}), MatListItem;

Additionally, Ivy contains factory code (the \u0275u0275defineComponent calls) for MatNavList, MatList, and MatListItem, but VE only contains the equivalent code (createRendererType2 calls) for MatNavList and MatListItem. Since these factories contain a large amount of css code, the bundle size increase is noticeable.

@jtan80813
Copy link

jtan80813 commented Mar 23, 2020

@filipesilva. Any updates on this? I have same issues with them above. My main.js is bigger but the bundle size is smaller. I have implemented the steps in https://angular.io/guide/ivy-compatibility#payload-size-debugging.

I have put {"sideEffects": false} and the bundle size became smaller but my app only shows the loading screen too so i just remove it.

Is there a problem on Angular 9 Ivy?
Hoping that this will be fixed.

Thanks a lot

@filipesilva
Copy link
Contributor

@jtan80813 we're still looking into how we can address the particular problem in this issue, but things are moving along.

@jtan80813
Copy link

@filipesilva. Thats great! Pls update us here if its fixed. Thanks!

@jbjhjm
Copy link

jbjhjm commented Apr 8, 2020

Same issue here:

  1. overall build size without Gzip: 3.48 MB with Ivy, 3.25 MB with ViewEngine

  2. MAIN bundle size without Gzip: 2.17 MB with Ivy, 1.42 MB with ViewEngine

  3. @angular chunk size without GZip: 498 kB with Ivy, 477 kB with ViewEngine

... which is due to Ivy putting more features into synchronous chunk than ViewEngine.

Bundle with Ivy:
frontend_ivy

Bundle with ViewEngine:
frontend_classic

Angular CLI: 9.0.7
Node: 10.16.0
OS: win32 x64

Angular: 9.0.7
... animations, cli, common, compiler, compiler-cli, core, forms
... language-service, platform-browser, platform-browser-dynamic
... router
Ivy Workspace: Yes

Package                           Version
-----------------------------------------------------------
@angular-devkit/architect         0.900.1
@angular-devkit/build-angular     0.900.7
@angular-devkit/build-optimizer   0.900.7
@angular-devkit/build-webpack     0.900.7
@angular-devkit/core              9.0.7
@angular-devkit/schematics        9.0.7
@angular/cdk                      9.1.3
@ngtools/webpack                  9.0.7
@schematics/angular               8.0.0
@schematics/update                0.900.7
rxjs                              6.5.4
typescript                        3.7.5
webpack                           4.41.2

@naveedahmed1
Copy link

Any update on this, I think Material is the most affected:

Component 	VE			IVY
tabs		16.75		>    	46.27kb
select		14.14		>	24.75kb
sidenav		9.65		>	23.32kb
dialog		3.14		>	12.29kb
cdk		87		>	97kb
forms		42		>	55kb
router		68		>	71kb
platform-browser23		>	86kb


@naveedahmed1
Copy link

The latest version of Lighthouse (6) now shows that there's still potential of optimization in main bundle, probably due to this issue?

lighthouse-js-minify

@naveedahmed1
Copy link

Anyone got a chance to run Coverage Tool (https://developers.google.com/web/tools/chrome-devtools/coverage) in Chrome Dev Tools for Angular app?

After running this against my website and navigating to all sections of the website it says "32% belongs to functions that have not yet been executed" which means that of 1.10Mb main bundle, ~360kb should have been tree-shaken.

Right? or am I misinterpreting it?

@Splaktar
Copy link
Contributor

Right? or am I misinterpreting it?

It's not clear how foolproof that coverage tool is. Wouldn't you need to have it navigate through and use every function in your app to be able to determine the true coverage?

Does it allow you to dig deeper to see which modules/classes/functions/etc aren't used?

@IgorMinar
Copy link
Contributor

Just to clarify my last comment - just because a coverage based tool identifies code as dead, it doesn't mean that the code is eligible for tree-shaking and dead code elimination. The current state of tooling in the JavaScript ecosystem is still very primitive, and in order to remove unused code, we have to refactor the code in complex ways to enable the tooling to process is correctly.

Example:

function alwaysUsed() {
  if (isFancyApp()) {
    doSomethingRare()
  }
}

function doSomethingRare() {
  // ... lots of code here...
}

function isFancyApp() {
  //  ... some custom condition ...
}

In the code above there is no easy way for the build tooling to understand how the isFancyAppcustom condition evaluates in a particular app. And therefore all functions will always be retained and never dead-code-elimited. However a coverage based tool will correctly identify that a lot of your bundle is unused. Fixing this structural code issue is hard, but possible. It just takes a lot of time, and we've spent the last several years fixing issues like this all over Angular the Framework, Angular Material, RxJS, TypeScript, and many other libraries commonly used in Angular apps.

devversion referenced this issue in devversion/material2 Jun 8, 2020
Angular Material list items currently optionally inject a parent `MatNavList`
or `MatList`. This has the downside of retaining these tokens at runtime
because they are used for dependency injection as values.

We can improve this by using so-called light-weight tokens. These allow
us to continue injecting parent list or nav-lists, but without the
risk of retaining the `MatList` and `MatNavList` classes with their
factories.

This was already the case before Angular v9 with View Engine, but
the issue significance increases with Ivy where factories are now
directly attached to the classes (such as `MatList` or `MatNavList`).

Using light-weight tokens avoids this issue and gives us an additional
size improvement. Notably this won't be an improvement when an
application uses both the nav-list and standard `MatList`.

Related to https://github.com/angular/angular-cli/issues/16866. More
context on light-weight tokens in:
https://hackmd.io/@mhevery/SyqDjUlrU#.
devversion referenced this issue in devversion/material2 Jun 8, 2020
Angular Material list items currently optionally inject a parent `MatNavList`
or `MatList`. This has the downside of retaining these tokens at runtime
because they are used for dependency injection as values.

We can improve this by using so-called light-weight tokens. These allow
us to continue injecting parent list or nav-lists, but without the
risk of retaining the `MatList` and `MatNavList` classes with their
factories.

This was already the case before Angular v9 with View Engine, but
the issue significance increases with Ivy where factories are now
directly attached to the classes (such as `MatList` or `MatNavList`).

Using light-weight tokens avoids this issue and gives us an additional
size improvement. Notably this won't be an improvement when an
application uses both the nav-list and standard `MatList`.

Related to https://github.com/angular/angular-cli/issues/16866. More
context on light-weight tokens in:
https://hackmd.io/@mhevery/SyqDjUlrU#.
devversion referenced this issue in devversion/material2 Jun 8, 2020
Angular Material list items currently optionally inject a parent `MatNavList`
or `MatList`. This has the downside of retaining these tokens at runtime
because they are used for dependency injection as values.

We can improve this by using so-called light-weight tokens. These allow
us to continue injecting parent list or nav-lists, but without the
risk of retaining the `MatList` and `MatNavList` classes with their
factories.

This was already the case before Angular v9 with View Engine, but
the issue significance increases with Ivy where factories are now
directly attached to the classes (such as `MatList` or `MatNavList`).

Using light-weight tokens avoids this issue and gives us an additional
size improvement. Notably this won't be an improvement when an
application uses both the nav-list and standard `MatList`.

Related to https://github.com/angular/angular-cli/issues/16866. More
context on light-weight tokens in:
https://hackmd.io/@mhevery/SyqDjUlrU#.
devversion referenced this issue in devversion/material2 Jun 8, 2020
Angular Material list items currently optionally inject a parent `MatNavList`
or `MatList`. This has the downside of retaining these tokens at runtime
because they are used for dependency injection as values.

We can improve this by using so-called light-weight tokens. These allow
us to continue injecting parent list or nav-lists, but without the
risk of retaining the `MatList` and `MatNavList` classes with their
factories.

This was already the case before Angular v9 with View Engine, but
the issue significance increases with Ivy where factories are now
directly attached to the classes (such as `MatList` or `MatNavList`).

Using light-weight tokens avoids this issue and gives us an additional
size improvement. Notably this won't be an improvement when an
application uses both the nav-list and standard `MatList`.

Related to https://github.com/angular/angular-cli/issues/16866. More
context on light-weight tokens in:
https://hackmd.io/@mhevery/SyqDjUlrU#.
devversion referenced this issue in devversion/material2 Jun 8, 2020
Angular Material list items currently optionally inject a parent `MatNavList`
or `MatList`. This has the downside of retaining these tokens at runtime
because they are used for dependency injection as values.

We can improve this by using so-called light-weight tokens. These allow
us to continue injecting parent list or nav-lists, but without the
risk of retaining the `MatList` and `MatNavList` classes with their
factories.

This was already the case before Angular v9 with View Engine, but
the issue significance increases with Ivy where factories are now
directly attached to the classes (such as `MatList` or `MatNavList`).

Using light-weight tokens avoids this issue and gives us an additional
size improvement. Notably this won't be an improvement when an
application uses both the nav-list and standard `MatList`.

Related to https://github.com/angular/angular-cli/issues/16866. More
context on light-weight tokens in:
https://hackmd.io/@mhevery/SyqDjUlrU#.
devversion referenced this issue in devversion/material2 Jun 9, 2020
Angular Material list items currently optionally inject a parent `MatNavList`
or `MatList`. This has the downside of retaining these tokens at runtime
because they are used for dependency injection as values.

We can improve this by using so-called light-weight tokens. These allow
us to continue injecting parent list or nav-lists, but without the
risk of retaining the `MatList` and `MatNavList` classes with their
factories.

This was already the case before Angular v9 with View Engine, but
the issue significance increases with Ivy where factories are now
directly attached to the classes (such as `MatList` or `MatNavList`).

Using light-weight tokens avoids this issue and gives us an additional
size improvement. Notably this won't be an improvement when an
application uses both the nav-list and standard `MatList`.

Related to https://github.com/angular/angular-cli/issues/16866. More
context on light-weight tokens in:
https://hackmd.io/@mhevery/SyqDjUlrU#.
devversion referenced this issue in devversion/angular Jun 9, 2020
Currently Angular internally already handles `InjectionToken` as
predicates for queries. This commit exposes this as public API
as people already relied on this functionality but currently used
workarounds to satisfy the type constraints (e.g. `as any`).

We intend to make this public as it low-effort, and it's a signficant
key part for the use of light-weight tokens as described in the upcoming
guide: angular#36144.

In concrete, applications might use injection tokens over class for both DI
and queries, because otherwise such class references for optional queries or DI
always cause these classes to be retained. This was also an issue in View Engine,
but now with Ivy, this pattern became worse as factories could be directly attached
to retained classes (ultimately ending up in the production bundle, while unused).

More details in the light-weight token guide and in:
https://github.com/angular/angular-cli/issues/16866.

Closes angular#21152.
devversion referenced this issue in devversion/angular Jun 9, 2020
Currently Angular internally already handles `InjectionToken` as
predicates for queries. This commit exposes this as public API
as people already relied on this functionality but currently used
workarounds to satisfy the type constraints (e.g. `as any`).

We intend to make this public as it low-effort, and it's a signficant
key part for the use of light-weight tokens as described in the upcoming
guide: angular#36144.

In concrete, applications might use injection tokens over class for both DI
and queries, because otherwise such class references for optional queries or DI
always cause these classes to be retained. This was also an issue in View Engine,
but now with Ivy, this pattern became worse as factories could be directly attached
to retained classes (ultimately ending up in the production bundle, while unused).

More details in the light-weight token guide and in:
https://github.com/angular/angular-cli/issues/16866.

Closes angular#21152.
devversion referenced this issue in devversion/angular Jun 9, 2020
Currently Angular internally already handles `InjectionToken` as
predicates for queries. This commit exposes this as public API as
developers already relied on this functionality but currently use
workarounds to satisfy the type constraints (e.g. `as any`).

We intend to make this public as it's low-effort to support, and
it's a significant key part for the use of light-weight tokens as
described in the upcoming guide: angular#36144.

In concrete, applications might use injection tokens over classes
for both optional DI and queries, because otherwise such references
cause classes to be always retained. This was also an issue in View
Engine, but now with Ivy, this pattern became worse, as factories are
directly attached to retained classes (ultimately ending up in the
production bundle, while being unused).

More details in the light-weight token guide and in: https://github.com/angular/angular-cli/issues/16866.

Closes angular#21152. Related to angular#36144.
devversion referenced this issue in devversion/angular Jun 10, 2020
Currently Angular internally already handles `InjectionToken` as
predicates for queries. This commit exposes this as public API as
developers already relied on this functionality but currently use
workarounds to satisfy the type constraints (e.g. `as any`).

We intend to make this public as it's low-effort to support, and
it's a significant key part for the use of light-weight tokens as
described in the upcoming guide: angular#36144.

In concrete, applications might use injection tokens over classes
for both optional DI and queries, because otherwise such references
cause classes to be always retained. This was also an issue in View
Engine, but now with Ivy, this pattern became worse, as factories are
directly attached to retained classes (ultimately ending up in the
production bundle, while being unused).

More details in the light-weight token guide and in: https://github.com/angular/angular-cli/issues/16866.

Closes angular#21152. Related to angular#36144.
devversion referenced this issue in devversion/angular Jun 10, 2020
Currently Angular internally already handles `InjectionToken` as
predicates for queries. This commit exposes this as public API as
developers already relied on this functionality but currently use
workarounds to satisfy the type constraints (e.g. `as any`).

We intend to make this public as it's low-effort to support, and
it's a significant key part for the use of light-weight tokens as
described in the upcoming guide: angular#36144.

In concrete, applications might use injection tokens over classes
for both optional DI and queries, because otherwise such references
cause classes to be always retained. This was also an issue in View
Engine, but now with Ivy, this pattern became worse, as factories are
directly attached to retained classes (ultimately ending up in the
production bundle, while being unused).

More details in the light-weight token guide and in: https://github.com/angular/angular-cli/issues/16866.

Closes angular#21152. Related to angular#36144.
@IgorMinar IgorMinar changed the title Increased initial main.js bundle size in v9 - mainly due to @angular modules Increased initial main.js bundle size in v9 - mainly due to @angular/material packages Jun 11, 2020
@IgorMinar IgorMinar transferred this issue from angular/angular-cli Jun 11, 2020
@IgorMinar
Copy link
Contributor

I'm transferring this to the components repo because most of the actions need to be taken in the @angular/material and @angular/cdk codebases (in addition to landing angular/angular#37506 in the core)

@devversion devversion self-assigned this Jun 11, 2020
@andrewseguin andrewseguin added the area: build & ci Related the build and CI infrastructure of the project label Jun 12, 2020
@devversion devversion added the in progress This issue is currently in progress label Jun 13, 2020
@devversion
Copy link
Member

@naveedahmed1 Do you have any application we could look into? @filipesilva's investigation on list seems correct, but I'd like to double-check on other components you mentioned. e.g. tabs

@naveedahmed1
Copy link

Thanks @devversion I have added you to a private repo, will it work?

@devversion
Copy link
Member

devversion commented Jun 17, 2020

@naveedahmed1 yeah, that will work, thanks! your help on this is much appreciated!

@naveedahmed1
Copy link

naveedahmed1 commented Jun 17, 2020

@devversion most welcome. If you receive error of No Providers for MatDialog, it would be due to this issue #19335, just add MatDialogModule to the providers and it should work.

@devversion
Copy link
Member

@naveedahmed1 I looked into it. Thanks for sharing your numbers and app. It looks like View Engine and Ivy deviate that much in your comparison because factories are directly attached to the classes, while View Engine had a separate chunk for factories (e.g. 24-es2015.x.js in your app). if we sum these numbers up for tabs, we'll only see a size difference of ~4kb, compared to the mentioned ~29kb.

Percentage-wise, these 4kb are still noticeable as that signifies a ~12% increase. The actual size differences come from factories being generated for directives. This wasn't the case in View Engine as there weren't any factories for directives. Instead, host bindings, DI etc. were directly inlined to the host factory if a directive is used. A directive factory in Ivy by average seems to be larger by around ~450b (compared to the VE inlined code). There seem to be 9 directive (base) classes in tabs, resulting in the ~4kb increase. The actual size benefit with Ivy surfaces here as multiple use of, for example MatTabLabel is not resulting in multiple inlined factory code as with View Engine. Instead the attached factory is re-used in Ivy. That should definitely offset the 4kb, and should signify a larger size reduction in your overall application. I can definitely observe this in your application. e.g. reduction of around 15% gzipped (and lot more without gzip).

@naveedahmed1
Copy link

@devversion thank you so much for your time and looking in to this, really appreciate.

@devversion
Copy link
Member

@naveedahmed1 Anytime! Thanks again for the great information/reproduction you provided! 🎉

I'm going to close this issue as we landed our refactor changes in Angular Components. See #19576. Other than that, a slight increase in Angular Components chunks might be noticeable "at first glance", but in reality this is due to Ivy's architecture which later on results in overall size benefits (due to reduced factory code duplication etc.). Please see my comment above for a concrete example.

Also if you continue to discover any significant size impact, please create a new issue! We'd love to look into it!

@angular-automatic-lock-bot
Copy link

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.

@angular-automatic-lock-bot angular-automatic-lock-bot bot locked and limited conversation to collaborators Jul 30, 2020
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
area: build & ci Related the build and CI infrastructure of the project in progress This issue is currently in progress
Projects
None yet
Development

No branches or pull requests

10 participants