Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Angular tree shaking not stripping dev code, what things should I look for?

I'm using the latest versions of all Angular-related packages (so Angular 10).

I want to add some code to a component, but I only want this code to exist in dev, never in a production build. It needs to be completely stripped in prod builds. I found this comment, which indicates that environments do this automatically (because they're const).

I tried using that exact code in my app, but the dev code is still there in a production build. I copied the code over to a new test app that I made with ng new, and it does work properly there.

What things should I be looking for, how can I fix this? Is this possibly because I have CommonJS dependencies, and if so, can I do anything about that (since I can't remove those dependencies)?

Some notes:

  • An issue has been opened on the angular-cli repo here.
  • The environment object is never written to anywhere in the codebase, I've searched thoroughly. (It's only used in a few places anyway.)
  • Code bounded with if (false) { } is properly stripped.
  • Removing the services export from the end of environment{.prod}.ts does not fix the problem.
  • Removing all CommonJS dependencies does not fix the problem.

Here's environment.prod.ts (environment.ts is the same, just with false instead of true):

export const environment = {
  production: true
};

export * from './services/services';

Here's the main.ts that I'm testing with:

import { enableProdMode } from '@angular/core';
import { platformBrowserDynamic } from '@angular/platform-browser-dynamic';
import { environment } from 'environments/environment';
import { AppModule } from './app/app.module';

// tslint:disable:no-console

if (environment.production) {
  console.warn('this is a prod build');
  enableProdMode();
}

if (!environment.production) {
  console.warn('this is a dev build');
}

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

Here's the relevant output code after running ng build -c my-prod-config:

o.X.production && (console.warn('this is a prod build'), Object(i.R) ()),
o.X.production || console.warn('this is a dev build'),
s.d().bootstrapModule(fi).catch (e=>console.error(e))

Here's the relevant part of angular.json:

"my-prod-config": {
  "optimization": true,
  "outputHashing": "all",
  "sourceMap": false,
  "extractCss": true,
  "namedChunks": false,
  "aot": true,
  "extractLicenses": true,
  "vendorChunk": false,
  "buildOptimizer": true,
  "stylePreprocessorOptions": {
    "includePaths": [
      "src/styles"
    ]
  },
  "fileReplacements": [
    {
      "replace": "src/environments/environment.ts",
      "with": "src/environments/environment.prod.ts"
    }
  ],
  "baseHref": "./"
}

Here's tsconfig.base.json:

{
  "compileOnSave": false,
  "compilerOptions": {
    "downlevelIteration": true,
    "importHelpers": true,
    "module": "es2020",
    "outDir": "./dist/out-tsc",
    "sourceMap": true,
    "declaration": false,
    "moduleResolution": "node",
    "baseUrl": "src/",
    "experimentalDecorators": true,
    "allowJs": true,
    "target": "es2015",
    "lib": [
      "es2018",
      "dom"
    ],
    "paths": {
      "path1": [
        "app/modules/stripped-from-stack-overflow-example1"
      ],
      "path2": [
        "app/modules/stripped-from-stack-overflow-example2"
      ]
    }
  },
  "files": [
    "src/main.ts",
    "src/polyfills.ts"
  ],
  "angularCompilerOptions": {
    "fullTemplateTypeCheck": true,
    "strictTemplates": true,
    "strictInjectionParameters": true
  }
}

Here's package.json:

{
  "name": "my-app",
  "version": "0.0.0",
  "license": "MIT",
  "scripts": {
    "section stripped": "section stripped"
  },
  "private": true,
  "dependencies": {
    "@angular/animations": "10.0.8",
    "@angular/common": "10.0.8",
    "@angular/compiler": "10.0.8",
    "@angular/core": "10.0.8",
    "@angular/forms": "10.0.8",
    "@angular/platform-browser": "10.0.8",
    "@angular/platform-browser-dynamic": "10.0.8",
    "@angular/router": "10.0.8",
    "@ng-idle/core": "9.0.0-beta.1",
    "@ng-idle/keepalive": "9.0.0-beta.1",
    "@ngneat/until-destroy": "8.0.1",
    "angular-svg-icon": "10.0.0",
    "brace": "0.11.1",
    "caniuse-lite": "1.0.30001111",
    "chart.js": "2.9.3",
    "core-js": "3.6.5",
    "css-vars-ponyfill": "2.3.2",
    "detect-browser": "5.1.1",
    "element-closest-polyfill": "1.0.2",
    "file-saver": "2.0.2",
    "fomantic-ui": "2.8.6",
    "jsonexport": "3.0.1",
    "moment": "2.24.0",
    "ngx-drag-drop": "2.0.0",
    "rxjs": "6.6.2",
    "tslib": "^2.0.0",
    "typeface-roboto": "0.0.75",
    "uuid": "8.3.0",
    "zone.js": "0.10.3"
  },
  "devDependencies": {
    "@angular-devkit/build-angular": "0.1000.5",
    "@angular/cli": "10.0.5",
    "@angular/compiler-cli": "10.0.8",
    "@angular/language-service": "10.0.8",
    "@types/chart.js": "2.7.54",
    "@types/file-saver": "2.0.1",
    "@types/uuid": "8.0.1",
    "codelyzer": "^6.0.0",
    "rimraf": "3.0.2",
    "rxjs-tslint-rules": "4.34.0",
    "ts-node": "8.10.2",
    "tslint": "6.1.3",
    "tslint-angular": "3.0.2",
    "typescript": "3.9.7",
    "webpack-bundle-analyzer": "3.8.0"
  }
}
like image 263
vaindil Avatar asked Aug 14 '20 14:08

vaindil


People also ask

Does Angular do Tree Shaking?

By default, Syncfusion Angular components supports Tree Shaking and it dose not require any special changes in application level.

What is Tree Shaking and why is it important?

Tree shaking is a term commonly used within a JavaScript context to describe the removal of dead code. It relies on the import and export statements to detect if code modules are exported and imported for use between JavaScript files.

Why providedIn services are Shakable?

When using providedIn on services, they will only be bundled in the application if they get used. These are called tree-shakable providers.

What do you understand by tree Shakeable providers?

Tree Shakeable Providers are a way to define services and other things to be used by Angular's dependency injection system in a way that can improve the performance of an Angular application.


3 Answers

This question was answered by an Angular team member here on GitHub. The answer is that this is a Webpack issue--if the environment file is imported into multiple output files, then Webpack is unable to optimize it properly. I've pasted the full response below for posterity.

Without a reproduction the definitive cause is hard to discern. However, a potential cause is the use of the environment JS module (environment.ts/environment.prod.ts) in more than one generated output file. This could be the case if the environment module is used in the main code and in the code for a lazy route. When this happens, Webpack cannot concatenate the environment module with the main module (as happens in a new project) because the environment module needs to be accessible to two different output modules. This then in turn prevents the optimizer from inlining the production property value since the environment object is now essentially an import from another module and not a local variable.

When this happens code similar to the following (which represents a separate Webpack module) should end up in the main output file for the application:

AytR: function (module, __webpack_exports__, __webpack_require__) {
  "use strict";
  __webpack_require__.d(__webpack_exports__, "a", function () {
    return environment;
  });
  const environment = { production: !0 };
},
like image 71
vaindil Avatar answered Oct 23 '22 21:10

vaindil


You could apply the same logic as environment.ts; create main.prod.ts (without the dev specific code) and main.dev.ts (with dev specific code), then use fileReplacements in your config.

The config for prod would be:

 "fileReplacements": [
      ...
      {
        "replace": "src/main.ts",
        "with": "src/main.prod.ts"
      }
like image 23
Albondi Avatar answered Oct 23 '22 21:10

Albondi


The post that you linked to specifically states that the tree-shaking occurs for 'Code gated by constants in if statements' . So you may need to alter your if statement to:

if (environment.production===true) {
  console.warn('this is a prod build');
  enableProdMode();
}
else    
{
  console.warn('this is a dev build');
}

to introduce the presence of a constant.

like image 1
Boluc Papuccuoglu Avatar answered Oct 23 '22 21:10

Boluc Papuccuoglu