Skip to content

Increased main.js bundle size but less lazy chunks, another case with Angular v9-rc.11 in a monorepo structure聽#16799

Closed

Description

@dogmatico

馃悶 Bug report

Command (mark with an x)

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

Is this a regression?

Yes, with Angular v8, or Ivy disabled, the main bundle size issue did not happen

Description

Like in issue #16146, Ivy enabled builds result in bigger main bundles.

We use an app-shell plus lazy load of components- with Angular Elements like in aio, and routes. With View Engine (VE), both in Angular 8 and 9, the main bundle effectively only included what was required to render the shell:

  • Core framework,
  • Auth back-end,
  • Angular Material sidenav and toolbar,
  • Shell components.

Once Ivy is enabled, it includes additional code. E.g., in one application, we get those two source-map-explorer results:

Captura de pantalla de 2020-01-30 09-41-47
Captura de pantalla de 2020-01-30 09-41-49

In other app, the result is also bigger main bundles. In particular, it adds all the @Injectable({providedIn: 'root}) services that are provided in a data-access internal library:

Captura de pantalla de 2020-01-30 10-45-15
Captura de pantalla de 2020-01-30 10-51-58

馃敩 Minimal Reproduction

The issue is in a proprietary mono-repo of front-ends. We can provide access in Bitbucket or Github private repositories if necessary. To generate the bundles, we used

ng build <project-name> --prod --source-map --localize=false

And only changed the tsconfig.json to set angularCompilerOptions.enableIvy = false

馃實 Your Environment




Angular CLI: 9.0.0-rc.11
Node: 12.14.1
OS: linux x64

Angular: 9.0.0-rc.11
... animations, cli, common, compiler, compiler-cli, core
... elements, forms, language-service, localize
... platform-browser, platform-browser-dynamic, platform-server
... router, service-worker
Ivy Workspace: No

Package                            Version
------------------------------------------------------------
@angular-devkit/architect          0.900.0-rc.11
@angular-devkit/build-angular      0.900.0-rc.11
@angular-devkit/build-ng-packagr   0.900.0-rc.11
@angular-devkit/build-optimizer    0.900.0-rc.11
@angular-devkit/build-webpack      0.900.0-rc.11
@angular-devkit/core               9.0.0-rc.11
@angular-devkit/schematics         9.0.0-rc.11
@angular/cdk                       9.0.0-rc.8
@angular/material                  9.0.0-rc.8
@angular/pwa                       0.803.23
@ngtools/webpack                   9.0.0-rc.11
@schematics/angular                8.3.23
@schematics/update                 0.900.0-rc.11
rxjs                               6.5.4
typescript                         3.7.5
webpack                            4.41.2

Activity

  1. IgorMinar commented on Jan 30, 2020

    @IgorMinar
    Contributor

    @angular/material is the most curious piece - it looks like with Ivy all of it is being retained in these apps. That's highly unusual.

    Is there any chance we could follow up offline and get a debug version of the bundle for us to analyze?

    Is there anything else atypical about your setup?

  2. added this to the v9-candidates milestone on Jan 30, 2020
  3. removed this from the v9-candidates milestone on Jan 30, 2020
  4. added this to the v9-candidates milestone on Jan 30, 2020
  5. removed this from the v9-candidates milestone on Jan 30, 2020
  6. modified the milestone: needsTriage on Jan 30, 2020
  7. modified the milestones: needsTriage, Backlog on Jan 30, 2020
  8. 9 remaining items

  9. filipesilva commented on Jan 31, 2020

    @filipesilva
    Contributor

    I also don't understand why I see so many of these warnings in the ivy compilation:

    WARNING in D:\sandbox\bmat-front-end-monorepo-angular\apps\reporting-tool\src\app\feature-lazy-channel-list\user-channel-tree\user-channel-tree.component.ts is part of the TypeScript compilation but it's unused.
    Add only entry points to the 'files' or 'include' properties in your tsconfig.
    
    WARNING in D:\sandbox\bmat-front-end-monorepo-angular\apps\reporting-tool\src\app\feature-lazy-channel-list\lazy-channel-list.module.ts is part of the TypeScript compilation but it's unused.
    Add only entry points to the 'files' or 'include' properties in your tsconfig.
    
    WARNING in D:\sandbox\bmat-front-end-monorepo-angular\apps\reporting-tool\src\app\feature-lazy-side-menu\side-menu\side-menu.component.ts is part of the TypeScript compilation but it's unused.
    Add only entry points to the 'files' or 'include' properties in your tsconfig.
    
    WARNING in D:\sandbox\bmat-front-end-monorepo-angular\apps\reporting-tool\src\app\feature-lazy-side-menu\lazy-side-menu.module.ts is part of the TypeScript compilation but it's unused.
    Add only entry points to the 'files' or 'include' properties in your tsconfig.
    
    WARNING in D:\sandbox\bmat-front-end-monorepo-angular\libs\auth\feature-angular-auth-user-apps\src\lib\feature-angular-auth-user-apps.component.ts is part of the TypeScript compilation but it's unused.
    Add only entry points to the 'files' or 'include' properties in your tsconfig.
    
    WARNING in D:\sandbox\bmat-front-end-monorepo-angular\libs\auth\feature-angular-auth-user-apps\src\lib\component\app-list-icon\app-list-icon.component.ts is part of the TypeScript compilation but it's unused.
    Add only entry points to the 'files' or 'include' properties in your tsconfig.
    

    This shouldn't happen because the tsconfig for this build contains explicit entry points:

    {
      "extends": "./tsconfig.json",
      "compilerOptions": {
        "outDir": "../../dist/out-tsc",
        "types": []
      },
      "files": ["src/main.ts", "src/polyfills.ts"],
      "exclude": ["src/test-setup.ts", "**/*.spec.ts"]
    }
    

    Ah but this one extends another one that actually has a broad include:

    {
      "extends": "../../tsconfig.json",
      "compilerOptions": {
        "types": ["node", "jest"],
        "jsx": "react",
        "jsxFactory": "h"
      },
      "include": ["**/*.ts", "**/*.tsx"]
    }
    

    Removing that include removes the warnings, but doesn't affect file size.

    Double checked my assertion that @bmat/reporting-tool/reporting-tool-interfaces was only found in a single file and found that to be false, I must have used a bad search before. It's found in loads of files, including a lot in libs/reporting-tool/data-access-reporting-tool-api. That is why it gets included.

  10. filipesilva commented on Jan 31, 2020

    @filipesilva
    Contributor

    Tried completely removing all imports from libs/reporting-tool/data-access-reporting-tool-api/src/index.ts to see if the presence of more imports there made a difference. The main bundle gets 1kb smaller and sourcemaps show the only things from libs/reporting-tool/data-access-reporting-tool-api/src/ are the really imported ones (libs/reporting-tool/data-access-reporting-tool-api/src/lib/reporting-tool-data-access-reporting-tool-api.module.ts).

    Although this was just a 1kb difference, there's something odd here. Importing a re-export from a index file shouldn't cause some unrelated files to stay in the compilation.

    Knowing this, I went back to libs/reporting-tool/reporting-tool-interfaces/src/lib/production-category-models.ts, which was included because of libs/reporting-tool/reporting-tool-interfaces/src/index.ts. I still don't know what's importing that last one. But I'm pretty sure that if I comment it's contents out, the TS compiler will error out and tell me.

    Sure enough, it points me towards libs/search-filters/feature-custom-filters/src/lib/filters-dialogs-list-item/filters-dialogs-list-item.component.ts, which imports two enums that are in libs/reporting-tool/reporting-tool-interfaces/src/lib/reporting-tool-reporting-tool-interfaces.ts, and re-exported through said index.ts.

    I comment out everything in libs/reporting-tool/reporting-tool-interfaces/src/index.ts except the export that's actually used, and again main drops in size, this time by ~60kb. It's still not on par with the VE main file, but it's getting closer.

    So there's definitely something going on with re-exports. In some causes they get retained in Ivy, but not in VE.

  11. filipesilva commented on Jan 31, 2020

    @filipesilva
    Contributor

    Ok, I was able to destil this down to a minimal repro:


    BarrelSideEffects

    This repository reproduces the problem in #16799.

    git clone https://github.com/filipesilva/barrel-side-effects
    npm install
    npm run repro
    

    What's happening?

    This app contains the following files:

    • src/app/another/index.ts
    export * from './another.module';
    export * from './side-effects';
    • src/app/another/another.module.ts
    import { NgModule } from '@angular/core';
    import { CommonModule } from '@angular/common';
    
    @NgModule({
      declarations: [],
      imports: [
        CommonModule
      ]
    })
    export class AnotherModule { }
    • src/app/another/side-effects.ts
    import { DateTime } from 'luxon';
    
    export const local = () => DateTime.local();
    export const map = new Map([['key', 'value']]);
    • src/app/app.module.ts
    import { BrowserModule } from '@angular/platform-browser';
    import { NgModule } from '@angular/core';
    
    import { AppComponent } from './app.component';
    import { AnotherModule } from './another/index';
    
    @NgModule({
      declarations: [
        AppComponent
      ],
      imports: [
        BrowserModule,
        AnotherModule,
      ],
      providers: [],
      bootstrap: [AppComponent]
    })
    export class AppModule { }

    These files are setup so that:

    • src/app/app.module.ts imports AnotherModule from src/app/another/index.ts
    • src/app/another/index.ts also exports src/app/another/side-effects.ts, a file that isn't used but loads a library (Luxon) and has a toplevel side effect (a Map instantiated with ['key', 'value']).

    npm run repro will build the application first with Ivy, and then with View Engine (VE, the compiler before Ivy).

    The Ivy build will contain the contents of src/app/another/side-effects.ts, but the VE build will not.
    You can tell because the Ivy build is much larger. You can also open dist/barrel-side-effects/main.js and find Luxon referenced there, along with map (as new Map([["key","value"]])).

  12. dogmatico commented on Feb 1, 2020

    @dogmatico
    Author

    Great, thanks for your deep analysis and minimal repo. I'll take a look; I'm not used to ngcc and build tool internals but anyway will try to see if I can help with this issue.

  13. filipesilva commented on Feb 1, 2020

    @filipesilva
    Contributor

    I'm not really sure what can be done about this, as it was a design choice. VE used deep imports because of the whole separate factory file business, and we also ended up removing unused imports after the decorators were stripped out of the original file, so index file were bypasses. But Ivy just uses the same file instead, so whatever imports are there, stay there.

    It's not strictly incorrect mind you. VE was doing something incorrect, insofar as any side effects or imports you had referenced in files re-exported from index files could be dropped. But your app might rely on them, so that wasn't right. Ivy does the right thing and doesn't mess with your import structure.

    But in cases like this, where there's a lot of shared code and the shared libraries are not free from side effects, extra code ends up being retained. You could hypothetically go around and use deep imports everywhere, but that's not great.

    There's something else you can do though. If your shared libraries are meant to be free from side effects, add "sideEffects": false to their package.json. You can also just drop a new package.json with {"sideEffects": false} on folders that you know are free from side effects, files in that folder and in any subfolder will be treated as such.

    The more I think about it, the more I that's the right thing to do in this case. My reasoning is this: VE erroneously caused some index files to be treated as though they had no side effects and left you no recourse, but Ivy doesn't do this and instead requires you to mark code free from side effects as such.

  14. dogmatico commented on Feb 3, 2020

    @dogmatico
    Author

    For most of our applications is not a big issue to have larger main bundles; those are b2b web application with service workers, hence it's fine to once in while fetch more data.

    Also, I noticed that patching the package.json of Luxon, or the Webpack configuration webpackConfig.resolve.mainFields of Angular CLI, to use Luxon ESM instead of CJS modules resulted in solving the issue for the reproduction repository. If the change in the behaviour of Ivy is intended and the correct one, we can see this issue as a bundler + app architecture problem and not an Angular one.

    Maybe we can add somewhere a mention to this change and why it's the correct behaviour compared to VE. It will save other users from opening issues like this one and may help them into finding ways to improve their bundle sizes.

    Again, thanks a lot for your deep review and comments.

  15. filipesilva commented on Feb 3, 2020

    @filipesilva
    Contributor

    Just to be clear, when I said

    There's something else you can do though. If your shared libraries are meant to be free from side effects, add "sideEffects": false to their package.json. You can also just drop a new package.json with {"sideEffects": false} on folders that you know are free from side effects, files in that folder and in any subfolder will be treated as such.

    The shared libraries I meant where the ones in your libs/ folder.

    It's not safe to patch the package.json of Luxon, because we don't know if it actually is free from side effects or not. It would be safe to use its ESM entry point (module in package.json), but we prioritize browser instead because often there are libraries that primarily have Node entry points and then put a browser entry point in browser.

  16. dogmatico commented on Feb 3, 2020

    @dogmatico
    Author

    Yes, all was understood. The patch was an experiment to set the ESM entry point instead of CJS one, not an actual code change nor sideEffects = false declaration, and see what happened.

    For my part, and considering that the Ivy behaviour is more reasonable, this issue can be closed. My only suggestion is, perhaps, explain this new treatment of the side effects in the https://next.angular.io/guide/ivy page.

    Thanks,
    Cristian

  17. DmitryEfimenko commented on Feb 5, 2020

    @DmitryEfimenko

    so does this mean that angular/forms should be marked as sideEffects = false?

  18. dogmatico commented on Feb 5, 2020

    @dogmatico
    Author

    so does this mean that angular/forms should be marked as sideEffects = false?

    In my particular case, @angular/forms was dragged to the main bundle by a custom library that provided a date/month picker and imported it. Once I checked that our own libraries were free from side effects, and marked it with module level package.json, it returned to similar a size and identical contents.

    We didn't patch any third party library, nor @angular packages to achieve it, and I wouldn't take that path unless strictly necessary.

  19. clydin commented on Feb 5, 2020

    @clydin
    Member

    The @angular/forms package is currently marked as side effect free. All the client-side @angular packages are marked with the exception of the @angular/compiler package which is not used in a production (AOT compiled) application.

  20. filipesilva commented on Feb 6, 2020

    @filipesilva
    Contributor
  21. angular-automatic-lock-bot commented on Mar 8, 2020

    @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.

  22. locked and limited conversation to collaborators on Mar 8, 2020
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

      Milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions