Conditionally imported modules are always included in the bundle

🐞 bug report

Description

The issue happens when using some condition to import a module:

const includeTestModule = false;
@NgModule({
  declarations: [AppComponent],
  imports: [BrowserModule, includeTestModule ? TestModule : []],
  bootstrap: [AppComponent],
})
export class AppModule {}

Even when includeTestModule = false the TestModule is included in the bundle. The only difference is that it’s not injected to the AppModule.
More details:

  • sideEffects: false flag is specified for the whole project
  • running build with --prod flag to turn on the optimizations

🔬 Minimal Reproduction

Reproduction repo

To replicate just build the project using npm run build.
You will see all the values from huge TestEnum in dist/main.<hash>.js which is only included in conditionally not imported module. You can also run npm run analyse after building the project to see the main bundle is 307KB.

When removing conditional import and TestModule from AppModule completely
BEFORE:

import { NgModule } from '@angular/core';
import { BrowserModule } from '@angular/platform-browser';
import { AppComponent } from './app.component';
import { TestModule } from './test.module';

const includeTestModule = false;
@NgModule({
  declarations: [AppComponent],
  imports: [BrowserModule, includeTestModule ? TestModule : []],
  bootstrap: [AppComponent],
})
export class AppModule {}

AFTER:

import { NgModule } from '@angular/core';
import { BrowserModule } from '@angular/platform-browser';
import { AppComponent } from './app.component';

@NgModule({
  declarations: [AppComponent],
  imports: [BrowserModule],
  bootstrap: [AppComponent],
})
export class AppModule {}

The size of the main bundle is then 94KB as there’s no big enum from TestModule.

🌍 Your Environment

Angular Version:




     _                      _                 ____ _     ___
    / \   _ __   __ _ _   _| | __ _ _ __     / ___| |   |_ _|
   / △ \ | '_ \ / _` | | | | |/ _` | '__|   | |   | |    | |
  / ___ \| | | | (_| | |_| | | (_| | |      | |___| |___ | |
 /_/   \_\_| |_|\__, |\__,_|_|\__,_|_|       \____|_____|___|
                |___/
    

Angular CLI: 11.2.3
Node: 14.15.5
OS: linux x64

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

Package                         Version
---------------------------------------------------------
@angular-devkit/architect       0.1102.3
@angular-devkit/build-angular   0.1102.3
@angular-devkit/core            11.2.3
@angular-devkit/schematics      11.2.3
@angular/cli                    11.2.3
@schematics/angular             11.2.3
@schematics/update              0.1102.3
rxjs                            6.6.6
typescript                      4.0.7

Anything else relevant?

stats.json record for types.ts where TestEnum is defined:


{
  "id": null,
  "identifier": "/home/smarkevic/ng-test/angular-conditional-import-issue/node_modules/@angular-devkit/build-angular/src/babel/webpack-loader.js??ref--7-0!/home/smarkevic/ng-test/angular-conditional-import-issue/node_modules/@angular-devkit/build-optimizer/src/build-optimizer/webpack-loader.js??ref--7-1!/home/smarkevic/ng-test/angular-conditional-import-issue/node_modules/@angular-devkit/build-optimizer/src/build-optimizer/webpack-loader.js??ref--16-0!/home/smarkevic/ng-test/angular-conditional-import-issue/node_modules/@ngtools/webpack/src/ivy/index.js!/home/smarkevic/ng-test/angular-conditional-import-issue/src/app/types.ts",
  "name": "./src/app/types.ts",
  "index": 56,
  "index2": 52,
  "size": 387829,
  "cacheable": true,
  "built": true,
  "optional": false,
  "prefetched": false,
  "chunks": [],
  "issuer": "/home/smarkevic/ng-test/angular-conditional-import-issue/node_modules/@angular-devkit/build-angular/src/babel/webpack-loader.js??ref--7-0!/home/smarkevic/ng-test/angular-conditional-import-issue/node_modules/@angular-devkit/build-optimizer/src/build-optimizer/webpack-loader.js??ref--7-1!/home/smarkevic/ng-test/angular-conditional-import-issue/node_modules/@angular-devkit/build-optimizer/src/build-optimizer/webpack-loader.js??ref--16-0!/home/smarkevic/ng-test/angular-conditional-import-issue/node_modules/@ngtools/webpack/src/ivy/index.js!/home/smarkevic/ng-test/angular-conditional-import-issue/src/app/test.module.ts",
  "issuerId": null,
  "issuerName": "./src/app/test.module.ts",
  "issuerPath": [
    {
      "id": 0,
      "identifier": "multi /home/smarkevic/ng-test/angular-conditional-import-issue/src/main.ts",
      "name": "multi ./src/main.ts",
      "profile": {
        "factory": 0,
        "building": 2
      }
    },
    {
      "id": null,
      "identifier": "/home/smarkevic/ng-test/angular-conditional-import-issue/node_modules/@angular-devkit/build-angular/src/babel/webpack-loader.js??ref--7-0!/home/smarkevic/ng-test/angular-conditional-import-issue/node_modules/@angular-devkit/build-optimizer/src/build-optimizer/webpack-loader.js??ref--7-1!/home/smarkevic/ng-test/angular-conditional-import-issue/node_modules/@angular-devkit/build-optimizer/src/build-optimizer/webpack-loader.js??ref--16-0!/home/smarkevic/ng-test/angular-conditional-import-issue/node_modules/@ngtools/webpack/src/ivy/index.js!/home/smarkevic/ng-test/angular-conditional-import-issue/src/main.ts",
      "name": "./src/main.ts",
      "profile": {
        "factory": 122,
        "building": 76
      }
    },
    {
      "id": null,
      "identifier": "/home/smarkevic/ng-test/angular-conditional-import-issue/node_modules/@angular-devkit/build-angular/src/babel/webpack-loader.js??ref--7-0!/home/smarkevic/ng-test/angular-conditional-import-issue/node_modules/@angular-devkit/build-optimizer/src/build-optimizer/webpack-loader.js??ref--7-1!/home/smarkevic/ng-test/angular-conditional-import-issue/node_modules/@angular-devkit/build-optimizer/src/build-optimizer/webpack-loader.js??ref--16-0!/home/smarkevic/ng-test/angular-conditional-import-issue/node_modules/@ngtools/webpack/src/ivy/index.js!/home/smarkevic/ng-test/angular-conditional-import-issue/src/app/app.module.ts",
      "name": "./src/app/app.module.ts",
      "profile": {
        "factory": 1036,
        "building": 4052,
        "dependencies": 935
      }
    },
    {
      "id": null,
      "identifier": "/home/smarkevic/ng-test/angular-conditional-import-issue/node_modules/@angular-devkit/build-angular/src/babel/webpack-loader.js??ref--7-0!/home/smarkevic/ng-test/angular-conditional-import-issue/node_modules/@angular-devkit/build-optimizer/src/build-optimizer/webpack-loader.js??ref--7-1!/home/smarkevic/ng-test/angular-conditional-import-issue/node_modules/@angular-devkit/build-optimizer/src/build-optimizer/webpack-loader.js??ref--16-0!/home/smarkevic/ng-test/angular-conditional-import-issue/node_modules/@ngtools/webpack/src/ivy/index.js!/home/smarkevic/ng-test/angular-conditional-import-issue/src/app/test.module.ts",
      "name": "./src/app/test.module.ts",
      "profile": {
        "factory": 935,
        "building": 9,
        "dependencies": 910
      }
    }
  ],
  "profile": {
    "factory": 899,
    "building": 882
  },
  "failed": false,
  "errors": 0,
  "warnings": 0,
  "assets": [],
  "reasons": [
    {
      "moduleId": null,
      "moduleIdentifier": "/home/smarkevic/ng-test/angular-conditional-import-issue/node_modules/@angular-devkit/build-angular/src/babel/webpack-loader.js??ref--7-0!/home/smarkevic/ng-test/angular-conditional-import-issue/node_modules/@angular-devkit/build-optimizer/src/build-optimizer/webpack-loader.js??ref--7-1!/home/smarkevic/ng-test/angular-conditional-import-issue/node_modules/@angular-devkit/build-optimizer/src/build-optimizer/webpack-loader.js??ref--16-0!/home/smarkevic/ng-test/angular-conditional-import-issue/node_modules/@ngtools/webpack/src/ivy/index.js!/home/smarkevic/ng-test/angular-conditional-import-issue/src/app/test.module.ts",
      "module": "./src/app/test.module.ts",
      "moduleName": "./src/app/test.module.ts",
      "type": "harmony side effect evaluation",
      "userRequest": "./types",
      "loc": "2:0-33"
    },
    {
      "moduleId": null,
      "moduleIdentifier": "/home/smarkevic/ng-test/angular-conditional-import-issue/node_modules/@angular-devkit/build-angular/src/babel/webpack-loader.js??ref--7-0!/home/smarkevic/ng-test/angular-conditional-import-issue/node_modules/@angular-devkit/build-optimizer/src/build-optimizer/webpack-loader.js??ref--7-1!/home/smarkevic/ng-test/angular-conditional-import-issue/node_modules/@angular-devkit/build-optimizer/src/build-optimizer/webpack-loader.js??ref--16-0!/home/smarkevic/ng-test/angular-conditional-import-issue/node_modules/@ngtools/webpack/src/ivy/index.js!/home/smarkevic/ng-test/angular-conditional-import-issue/src/app/test.module.ts",
      "module": "./src/app/test.module.ts",
      "moduleName": "./src/app/test.module.ts",
      "type": "harmony import specifier",
      "userRequest": "./types",
      "loc": "5:16-21"
    }
  ],
  "usedExports": true,
  "providedExports": [
    "TestEnum"
  ],
  "optimizationBailout": [],
  "depth": 4
}

2 thoughts on “Conditionally imported modules are always included in the bundle

  1. With help from @alan-agius4 we analysed this and the conclusion is that this isn’t working as you expected because of the reference to the namespace import:

    https://github.com/spwin/angular-conditional-import-issue/blob/f23da5a899bc5b71739fce2d3904fede0e5c4657/src/app/test.module.ts#L6

    Alan determined that this causes a module concatenation bailout as indicated by the stats information from Webpack, notice the optimizationBailout near the bottom:

    {
      "type": "module",
      "moduleType": "javascript/auto",
      "layer": null,
      "size": 387829,
      "sizes": {
        "javascript": 387829
      },
      "built": true,
      "codeGenerated": false,
      "cached": false,
      "identifier": "./node_modules/@angular-devkit/build-angular/src/babel/webpack-loader.js??ruleSet[1].rules[3].use[0]!./node_modules/@angular-devkit/build-optimizer/src/build-optimizer/webpack-loader.js??ruleSet[1].rules[3].use[1]!./node_modules/@angular-devkit/build-optimizer/src/build-optimizer/webpack-loader.js??ruleSet[1].rules[12].use[0]!./node_modules/@ngtools/webpack/src/ivy/index.js!./src/app/types.ts",
      "name": "./src/app/types.ts",
      "nameForCondition": "./src/app/types.ts",
      "index": 55,
      "preOrderIndex": 55,
      "index2": 52,
      "postOrderIndex": 52,
      "cacheable": true,
      "optional": false,
      "orphan": false,
      "dependent": true,
      "issuer": "./node_modules/@angular-devkit/build-angular/src/babel/webpack-loader.js??ruleSet[1].rules[3].use[0]!./node_modules/@angular-devkit/build-optimizer/src/build-optimizer/webpack-loader.js??ruleSet[1].rules[3].use[1]!./node_modules/@angular-devkit/build-optimizer/src/build-optimizer/webpack-loader.js??ruleSet[1].rules[12].use[0]!./node_modules/@ngtools/webpack/src/ivy/index.js!./src/app/test.module.ts",
      "issuerName": "./src/app/test.module.ts",
      "issuerPath": [
        {
          "identifier": "./node_modules/@angular-devkit/build-angular/src/babel/webpack-loader.js??ruleSet[1].rules[3].use[0]!./node_modules/@angular-devkit/build-optimizer/src/build-optimizer/webpack-loader.js??ruleSet[1].rules[3].use[1]!./node_modules/@angular-devkit/build-optimizer/src/build-optimizer/webpack-loader.js??ruleSet[1].rules[12].use[0]!./node_modules/@ngtools/webpack/src/ivy/index.js!./src/main.ts",
          "name": "./src/main.ts",
          "profile": {
            "total": 285,
            "resolving": 107,
            "restoring": 0,
            "building": 178,
            "integration": 0,
            "storing": 0,
            "additionalResolving": 0,
            "additionalIntegration": 0,
            "factory": 107,
            "dependencies": 0
          },
          "id": null
        },
        {
          "identifier": "./node_modules/@angular-devkit/build-angular/src/babel/webpack-loader.js??ruleSet[1].rules[3].use[0]!./node_modules/@angular-devkit/build-optimizer/src/build-optimizer/webpack-loader.js??ruleSet[1].rules[3].use[1]!./node_modules/@angular-devkit/build-optimizer/src/build-optimizer/webpack-loader.js??ruleSet[1].rules[12].use[0]!./node_modules/@ngtools/webpack/src/ivy/index.js!./src/app/app.module.ts",
          "name": "./src/app/app.module.ts",
          "profile": {
            "total": 37,
            "resolving": 3,
            "restoring": 0,
            "building": 34,
            "integration": 0,
            "storing": 0,
            "additionalResolving": 0,
            "additionalIntegration": 0,
            "factory": 3,
            "dependencies": 0
          },
          "id": null
        },
        {
          "identifier": "./node_modules/@angular-devkit/build-angular/src/babel/webpack-loader.js??ruleSet[1].rules[3].use[0]!./node_modules/@angular-devkit/build-optimizer/src/build-optimizer/webpack-loader.js??ruleSet[1].rules[3].use[1]!./node_modules/@angular-devkit/build-optimizer/src/build-optimizer/webpack-loader.js??ruleSet[1].rules[12].use[0]!./node_modules/@ngtools/webpack/src/ivy/index.js!./src/app/test.module.ts",
          "name": "./src/app/test.module.ts",
          "profile": {
            "total": 26,
            "resolving": 3,
            "restoring": 0,
            "building": 23,
            "integration": 0,
            "storing": 0,
            "additionalResolving": 0,
            "additionalIntegration": 0,
            "factory": 3,
            "dependencies": 0
          },
          "id": null
        }
      ],
      "failed": false,
      "errors": 0,
      "warnings": 0,
      "profile": {
        "total": 645,
        "resolving": 3,
        "restoring": 0,
        "building": 642,
        "integration": 0,
        "storing": 0,
        "additionalResolving": 0,
        "additionalIntegration": 0,
        "factory": 3,
        "dependencies": 0
      },
      "id": null,
      "issuerId": null,
      "chunks": [],
      "assets": [],
      "reasons": [
        {
          "moduleIdentifier": "./node_modules/@angular-devkit/build-angular/src/babel/webpack-loader.js??ruleSet[1].rules[3].use[0]!./node_modules/@angular-devkit/build-optimizer/src/build-optimizer/webpack-loader.js??ruleSet[1].rules[3].use[1]!./node_modules/@angular-devkit/build-optimizer/src/build-optimizer/webpack-loader.js??ruleSet[1].rules[12].use[0]!./node_modules/@ngtools/webpack/src/ivy/index.js!./src/app/test.module.ts",
          "module": "./src/app/test.module.ts",
          "moduleName": "./src/app/test.module.ts",
          "resolvedModuleIdentifier": "./node_modules/@angular-devkit/build-angular/src/babel/webpack-loader.js??ruleSet[1].rules[3].use[0]!./node_modules/@angular-devkit/build-optimizer/src/build-optimizer/webpack-loader.js??ruleSet[1].rules[3].use[1]!./node_modules/@angular-devkit/build-optimizer/src/build-optimizer/webpack-loader.js??ruleSet[1].rules[12].use[0]!./node_modules/@ngtools/webpack/src/ivy/index.js!./src/app/test.module.ts",
          "resolvedModule": "./src/app/test.module.ts",
          "type": "harmony side effect evaluation",
          "active": false,
          "explanation": "",
          "userRequest": "./types",
          "loc": "2:0-33",
          "moduleId": null,
          "resolvedModuleId": null
        },
        {
          "moduleIdentifier": "./node_modules/@angular-devkit/build-angular/src/babel/webpack-loader.js??ruleSet[1].rules[3].use[0]!./node_modules/@angular-devkit/build-optimizer/src/build-optimizer/webpack-loader.js??ruleSet[1].rules[3].use[1]!./node_modules/@angular-devkit/build-optimizer/src/build-optimizer/webpack-loader.js??ruleSet[1].rules[12].use[0]!./node_modules/@ngtools/webpack/src/ivy/index.js!./src/app/test.module.ts",
          "module": "./src/app/test.module.ts",
          "moduleName": "./src/app/test.module.ts",
          "resolvedModuleIdentifier": "./node_modules/@angular-devkit/build-angular/src/babel/webpack-loader.js??ruleSet[1].rules[3].use[0]!./node_modules/@angular-devkit/build-optimizer/src/build-optimizer/webpack-loader.js??ruleSet[1].rules[3].use[1]!./node_modules/@angular-devkit/build-optimizer/src/build-optimizer/webpack-loader.js??ruleSet[1].rules[12].use[0]!./node_modules/@ngtools/webpack/src/ivy/index.js!./src/app/test.module.ts",
          "resolvedModule": "./src/app/test.module.ts",
          "type": "harmony import specifier",
          "active": true,
          "explanation": "",
          "userRequest": "./types",
          "loc": "6:16-21",
          "moduleId": null,
          "resolvedModuleId": null
        }
      ],
      "usedExports": true,
      "providedExports": [
        "TestEnum"
      ],
      "optimizationBailout": [
        "Statement (ExportNamedDeclaration) with side effects in source code at 1:0-10002:7"
      ],
      "depth": 3
    }

    Inspecting the bundle also shows that the TestModule class itself was tree-shaken, just the imported namespace was not. This means that the conditional import is not relevant here. As a result, we believe there is no action item for us to take with respect to conditional NgModule imports.

    Thank you for the excellent reproduction by the way, that was really helpful to diagnose the issue.

  2. Just to add on what @JoostK mentioned.

    The problem is that you are referring the entire namespace in https://github.com/spwin/angular-conditional-import-issue/blob/f23da5a899bc5b71739fce2d3904fede0e5c4657/src/app/test.module.ts#L6.

    function testFactory() {
      // Just to use types somewhere in the module
      console.log(types);
    }

    The above cause Webpack not to concatenate the module and create a namespace object similar to the below

    // NAMESPACE OBJECT: ./src/app/types.ts
    var types_namespaceObject = {};
    __webpack_require__.r(types_namespaceObject);
    __webpack_require__.d(types_namespaceObject, "TestEnum", function() { return TestEnum; });

    The above, is non treeshakable because it’s not pure.

    Changing the syntax of the console.log to use an exported symbol from the namespace will cause not cause this bailout and the types module will be concatenated.

    function testFactory() {
      // Just to use types somewhere in the module
      console.log(types.TestEnum);
    }