question-mark
Stuck on an issue?

Lightrun Answers was designed to reduce the constant googling that comes with debugging 3rd party libraries. It collects links to all the places you might be looking at while hunting down a tough bug.

And, if you’re still stuck at the end, we’re happy to hop on a call to see how we can help out.

"ng build" not removing unneccessary code

See original GitHub issue

Which @angular/* package(s) are the source of the bug?

core

Is this a regression?

No

Description

When creating a new project via Angular (v14) CLI ng new test, paste the following code in any component and check the deployment’s output

constructor() {
  //#1 sure stays always
  console.log('THIS SHOULD HAPPEN');

  //#2 gets always removed
  if (false) { console.log('THIS SHOULDNT HAPPEN'); }

  //#3 doesn't get removed in my complex solution but only in a clean one
  if (!environment.production) { console.log('THIS SHOULDNT HAPPEN TOO'); }
}

angular/angular#1 & angular/angular#2 are removed from the code es expected. In my pretty complex solution (mixins, inheritences etc.) only angular/angular#2 gets removed - the !environment.production stays in the output! Can someone please help me find the reason for this? I’ve already tried pretty much but just can’t figure it out.

I asked for this on Stackoverflow (https://stackoverflow.com/questions/73318053/angular-14-ng-build-not-removing-unneccessary-code) already and was told to activate the old flags in the angular.json but, as expected, this didn’t work, too. Since I have absolutely no idea what could cause this, I consider it to be a bug (?). Or is this maybe Typescript related and I’d need to ask there?

Please provide a link to a minimal reproduction of the bug

No response

Please provide the exception or error you saw

No response

Please provide the environment you discovered this bug in (run ng version)

Angular CLI: 14.0.0
Node: 16.13.2
Package Manager: npm 8.5.4

Anything else?

No response

Issue Analytics

  • State:open
  • Created a year ago
  • Comments:6

github_iconTop GitHub Comments

2reactions
dgp1130commented, Sep 22, 2022

Discussed this in detail today and there are quite a few nuances here.

The root issue here is that Webpack chunks the application before it is fed into Terser to optimize. Webpack also uses a customized lazy-loading scheme (it’s not just a dynamic ESM import), which Terser doesn’t understand. As a result, Terser can’t effectively follow the dependency at build-time and be confident that it is a constant eligible for tree shaking.

There are a few potential options here. The first one is to make ngDevMode public API and your code can be re-written to if (ngDevMode) { /* ... */ }. This value is effectively substituted by the build process so Terser will resolve it as a constant true or false and can tree shake accordingly. We’ve been thinking about this for a while, but it hasn’t happened yet.

A second approach is to provide one more level of flexibility and allow custom substitutions, so users can specify logFromMyConstructor = true in some configuration file and be able to write if (logFromMyConstructor) { /* ... */ }. While a nice feature, our main concern is that when and where to use substitutions and its implications on tree shaking are very nuanced and hard to communicate and can be very easily misused. We’d also need to figure out typing since somewhere we need to include:

declare global {
  var logFromMyConstructor: boolean;
}

It’s not hard to include that in the project, but we want to make sure it plays well with the TypeScript language service, so this type definition needs to be statically analyzable.

This approach also begs the question of whether this substitution is done before or after the Angular compiler runs, which effectively asks the question “Should we allow substitutions to apply to Angular decorators?” For example, would it be valid to write:

@Component({
    // Conditionalize compile-time information (what template to use) on build substitution.
    template: useCoolTemplate ? 'cool.html' : 'less_cool.html',
})
class MyComponent { }

I believe you can do this today with environments as the Angular compiler will resolve such expression at compile time. I’m not sure that’s a pattern we want to encourage or propagate though.

A third option is to lean in to ESM formatting (particularly with the experimental ESBuild-er) and possibly get something more tree shakable. Since that uses native dynamic imports for chunking, it should be viable to statically analyze cross imports and identify compile-time constants. The challenge with this is that it’s often hard to prove that environments.production is truly constant, since any kind of foo[key] = {} statement might modify it if foo === environments and key === 'production', which can’t be disproven statically. This might be more viable if we updated environments to directly export their data instead of using an object.

// environments.ts
// Directly export values.
export const production = false;

// usage.ts
const { production } = await import('./chunk-abc123.js');
if (production) {
  // Definitely constant and tree shakable.
}

I’m not sure if ESBuild can actually do this today, but it seems like something it could feasibly do.

Fourth, we could do more of a redesign of environments to not require file replacements in the first place. We’re not 100% on that would look like, but it could be something that’s implemented as a build-time substitution or injected into the application semi-automatically.

Tangentially, I’m also thinking through whether or not we need environments to tree shake if we can provide the appropriate other tools like ngDevMode. Could diverging the concepts of “tree shake debug code” and “application configuration” still meet most user needs? These are just high-level thoughts, we need more investigation and probably an RFC to know if such a change would still meet the community’s needs.

Those are all big questions and ideas, but I think the most actionable part for this particular issue to work towards exposing ngDevMode as a public API. That should be easiest solution to land and would probably be sufficient for most use cases. If we still find users wanting to tree shake on environment data which doesn’t easily map to ngDevMode, we can explore some of those other solutions and see what the right approach is.

0reactions
pburgmercommented, Oct 10, 2022

Thanks for the detailed explanation @dgp1130. Please keep library authors in mind. I would like to get rid of some code in my library, if the application is compiled in prod mode. Environment replacement doesn’t work for libraries as far as I know. I would love to see ngDevMode as public API. Would it be possible to define it as a const, which can be imported from an EcmaScript module, instead of a global variable?

Read more comments on GitHub >

github_iconTop Results From Across the Web

Production build does not remove unused thirdparty code from ...
Looks like it's because lodash has not sideEffects: false in his package.json so webpack cant eliminate this import due to possible side effects ......
Read more >
angular ng build --prod doesn't ignore unused/unimported ...
The error posted implies that something is trying to instantiate that component but it is not referenced in any of the loaded ...
Read more >
How to remove unnecessary CSS in your Angular project
Remove unused CSS in Angular, Reach or any type of project using purifyCSS.
Read more >
Angular Removing Unused CSS and Obfuscate JavaScript in ...
This post will explain the Angular post-build process to remove unused CSS and hidden JavaScript files that enhance the application security ...
Read more >
Angular - How to remove and avoid unused imports
A common cause of bloated Angular applications is unneeded imports. And unused JavaScript dependencies. As an application grows, we developers ...
Read more >

github_iconTop Related Medium Post

No results found

github_iconTop Related StackOverflow Question

No results found

github_iconTroubleshoot Live Code

Lightrun enables developers to add logs, metrics and snapshots to live code - no restarts or redeploys required.
Start Free

github_iconTop Related Reddit Thread

No results found

github_iconTop Related Hackernoon Post

No results found

github_iconTop Related Tweet

No results found

github_iconTop Related Dev.to Post

No results found

github_iconTop Related Hashnode Post

No results found