angular-cli: Production build optimization breaks code

Bug Report or Feature Request (mark with an x)

- [x] bug report -> please search issues before submitting
- [ ] feature request

Area

- [x] devkit
- [ ] schematics

Versions

Windows node v9.4.0 npm v5.6.0

Repro steps

Use a third party library that contain non-side effect free getters. Run a production build. Observer that the code breaks at runtime, whereas it runs fine without “optimization”.

E.g.:

  • Scaffold a project that uses code (e.g. a third party library) that contains code that cannot be properly “optimized” using uglifyjs with it’s default settings. E.g. code that uses “non-pure getters” like this:
let object = {};
Object.defineProperty(object,"nonSideEffectFreeGetterBasedMember", 
    {get: function(){ console.log("side effect")}});

(function () {
  object.nonSideEffectFreeGetterBasedMember;
}());
  • Enable the angular build optimizer
  • Run the program with the build optimizer enabled and observe that the code does not work as expected anymore, because the non-pure getter has been removed and the side-effect does not happen anymore.

The log given by the failure

n/a - it’s a runtime issue in the application

Desired functionality

The optimization step should optimize, but never break code. The “pure_getters” option should be configurable and by default “false” should be used to guarantee best possible compatibility. If someone wants to optimize further and take the risk, that’s fine, but don’t break code by default: Don’t be optimistic or dictate how code should work in your opinion. Ideally make it possible to exclude specific libraries from this treatment. Users may have no control over these libraries and patching them often is not an option.

Mention any other details that might be useful

This has been reported before but was always closed without comments or with a comment that the third party code should be changed to workaround this limitation in angular. This is a one-liner fix in angular, though and instead of dictating how third party code should be written, the CLI should try not to break existing code, instead. Debugging “optimized production code” is a PITA the gains are minimal compared to that. Make this a configurable setting and make it possible to exclude certain files or third party libraries.

These are the previous issues that all had the same cause and where closed or ignored with no further explanation: https://github.com/angular/angular-cli/issues/9231 https://github.com/angular/angular-cli/issues/9221 https://github.com/angular/devkit/issues/937 https://github.com/angular/devkit/issues/388 https://github.com/angular/devkit/issues/612 and probably some more.

We are a library provider and we had several customers running into that issue. It’s a nightmare to debug and analyze and devs expect that there is no behavior change when the “optimizer” is turned on. But there can be and is a change: it breaks their projects in all kinds of ways.

Proposed workaround for people affected by this bug

Go and hack the buildoptimizer options in the code. Find the line that sets the pure_getters option to true or “strict” and set it to “false” instead. If there is no such line, make sure that “false” is used (because the default is true and breaks code).

About this issue

  • Original URL
  • State: closed
  • Created 6 years ago
  • Reactions: 30
  • Comments: 27 (7 by maintainers)

Commits related to this issue

Most upvoted comments

Since when closing tickets without resolution became a common practice? Please stop pretending that ugly workarounds are a solution and reopen the ticket.

@clydin - I always thought that Angular wanted to benefit from the node.js community and not dictate the community how they think JavaScript works and what subset of JavaScript should be supported in their opinion.

That is one of the most ignorant statements I have ever read: "Why should we fix our software when everyone else in the internet can just adjust (not fix - it is not broken) their codes. If you continue to follow this road, then this is the beginning of the end of angular.

If it only was about angular specific libraries. This is about all third party code on the internet and with the huge dependency trees you get nowadays (think leftpad), making sure that all your dependencies in your angular-cli application follow the NG-Script subset of EcmaScript will be too much effort for most devs and they will decide to either ditch the “optimization” or angular as a whole and switch to a framework that works with the community and not against it.

Sorry for the rant, but this is really the worst kind of reaction and attitude I could imagine, here. I always thought good of angular, but this really makes me doubt the whole project leadership.

Find here a list of libraries that are unlikely to be willing to “update their code to follow the recommended guidelines of the angular devs, and who would rather stick to the EcmaScript standard”. There might be a tiny fraction of about 0.1% or less of libraries on this list that have been included in error, feel free to fix it for the rest of them.

“Needs discussion” is a label that we review in our team meeting, happening now.

Here’s our resolution:

  • we pick some defaults that we think give the best experience for typical novice users.
  • if you want to customize those defaults, you have these options:
  1. Use ngx-build-plus to modify the webpack config and introduce whatever changes to the uglify config. Note this takes you off the supported path. See https://github.com/manfredsteyer/ngx-build-plus
  2. Opt-out of uglify optimizations by turning off build-optimizer in your angular.json or on the command-line
  3. Pass a custom webpack config to the build-webpack builder, see https://github.com/angular/angular-cli/blob/master/packages/angular_devkit/build_webpack/README.md

This is pathetic! Shame on your failure to provide a single argument besides “we the overlords have decided based on reasons that we dare to communicate”. Very disappointing and discurraging indeed…

Well, that is a very disappointing conclusion to this discussion. So basically we should go with:

  • Monkey-patching the CLI, loosing support and probably fighting with each update
  • Disable minification completely
  • Writing the build ourselves?

I don’t know guys, but this seems another case of forcing users to adapt a specific philosophy instead of listening to user feedback and adapting the product ever so slightly.

As mentioned previously, it is opt-out. Advanced optimizations (i.e., build optimizer) can be disabled if desired. Considering the small number of cases of this actually causing an issue (and not defects in uglify itself) and the ability to manually modify the setting if absolutely required via a post install step, there are no plans to change this.

I am really unable to figure out why this would be a stance. Giving finer control over the settings shouldn’t be a matter of this much debate. I as a developer would definitely want the choice to opt out of a feature that causes some of third party code to break.

I always thought Angular was supposed to be an inclusive community and not an exclusive one. As a developer, a lot of things would be out of my control when it comes to third party library. The framework should be there to support me in these situations by at least allowing us more control over the build process.

I decided to actually go ahead and test the size differences between bundles generated by three different approaches.

  • With build optimizer off - No loss in functionality - main.bundle.js = 11.16Mb
  • With build optimizer on - Unexpected behaviour shown by third party library - main.bundle.js=4.16Mb
  • With build optimizer on + using workaround suggested by @yGuy - No loss in functionality - main.bundle.js = 4.19Mb

So I do have the option to have no loss in functionality by having only a 30kb increase in my bundle, but the recommended solution by angular is to turn off the optimizer completely and deal with my bundle being almost 3x the size.

I really can’t wrap my head around this.

@yGuy I understand this issue is important to you, and I’ve certainly been on the receiving end of subtle breakages due to an optimizer (Closure Compiler’s advanced optimizations cause lots of these). That said, please try not to be hostile, we are all engineers searching for a good design for software that lots of people depend on, and have the best intentions.

Effort would most likely be better spent updating libraries/packages that are not following the recommended guidelines for advanced optimizations.

However, the team would be willing to consider a PR if a list was provided of libraries/packages that were not willing, or unable, to update their code to follow the recommended guidelines.

It is an important optimization and if absolutely essential can be disabled by turning off build-optimizer. There are no current plans to change this behavior.

Further, any code that has side effects in a property access is asking for trouble. It is error-prone, highly unintuitive and unexpected from a developers point of view. Essentially, regardless of the use of uglify and the option in question, a developers application will end up with unexpected behavior at some point as a result of a library ignoring general best practices.

And finally, never break code is an impossible metric. In many languages, use of exotic constructs will result in broken code when full optimizations are enabled. If code follows recommended practices full optimization can be enabled.

Based on this conversation:

  • there will be hundreds of thousands of developers spending long hours to debug the issue. There is no doubt the users of this library will hit this problem sooner or later.
  • we know that Angular team planned this to be fixed as a feature request - and that was 3 months ago.
  • we don’t know how to work around this

Switching optimization off is NOT an option. How to create a custom webpack config that will change pure_getters and retain all other default options? Angular ecosystem supposed to save millions of manhours from all the repetitive tasks such as writing configuration files.

I do think it is accurate to list those issues. I agree that for the angular/devkit#388 it may be wrong here. But as far as I can tell all of them would not exist if the buid optimizer would only optimize and not break the code. People tend to turn on optimization features (“why would it hurt?”) and then have no idea why their code breaks.

Which one of the issues I listed do you think would be there if the default settings would have been different?

Yes, there were resolutions to some of the issues. The resolution always was to disable the broken feature.

As such I agree that maybe this issue (and my “ignored/archived duplicate”) is the only issue that actually tries to fix the root problem, while all others just tried to get their code to run.

How great are these size savings? I feel they must be enormous if they outweight the price for broken, extremely hard to debug code. Can you share some details? One could argue if it is really safe to remove such code in some projects, then this is rather a problem of these projects, rather than a problem of the buildBreaker here.

In general I would really like to see the build optimizer focusing on “optimizing” the code that the user has control over and not some third party library code.

That the discussion is getting a bit heated is understandable, given the initial response to this issue. You may have the best intentions, but you can still be wrong. One response from your team suggested to rather “focus our efforts to patch all libraries in NPM that break due to this optimization” instead of acknowledging that it IS an issue and seems to come up regularly.

I stand by my previous point that this portion should be configurable, as uglifying code has been breaking stuff in JS for years now, proven by the fact that these plugins explicitly provide optional configuration to exclude stuff.

@alexeagle can you please consider an updated stance on this and reopen the issue? All the opinions (and thumbs up) above show the community wants this (seemingly easy to implement) change. Or can you at least provide a compelling reason not to do it?

I don’t see the point for patronising users by preventing us from configuring uglifyjs options. After all, we are all engineers with the best intentions. Your default options just don’t fit all use cases. Admitting this fact and letting us overwrite configuration options will empower everyone, including yourselves.

We use libraries like bitcoinjs quite often in our projects, and as Ionic has now migrated to Angular-CLI (which I think is a great change), the overly strict uglifying behavior breaks our application at runtime. See this issue thread: https://github.com/bitcoinjs/bitcoinjs-lib/issues/959

At least give us an option to properly configure the uglifier - It is one of the configuration flags that makes absolutely sense I think, in order to be compatible with the vast ecosystem provided by npm.

As a short term solution we do the following until we have time to create our own Angular build system:

npm install --save-dev replace

to install https://github.com/ALMaclaine/replace

Then, in package.json, we added a new script “disable-pure-getters” and run it before we build for production (excerpt):

"scripts": {
    "disable-pure-getters": "replace 'pure_getters: buildOptions.buildOptimizer' 'pure_getters: false' ./node_modules/@angular-devkit/build-angular/src/angular-cli-files/models/webpack-configs/common.js ",
    "build:prod": "npm run disable-pure-getters && ng build --prod ..."
}

The build server runs npm run build:prod. Super ugly, but it works with our Angular 6 version. You might need to change the path to the webpack-configs/common.js file for different Angular versions.

@clydin

May I kindly ask you to post the link to the official Angular “recommended guidelines for advanced optimizations”? Thank you.

From the UglifyJS documentation (https://github.com/mishoo/UglifyJS2):

pure_getters (default: “strict”) – If you pass true for this, UglifyJS will assume that object property access (e.g. foo.bar or foo[“bar”]) doesn’t have any side effects.

In my humble opinion the Angular CLI should not assume that code is written in a certain way. As @yGuy said, “The optimization step should optimize, but never break code.”

@alexeagle Thank you for chiming in. Maybe this is a case of “lost in translation”, but I really didn’t mean to be any more hostile than how I feel I’ve been treated here.

This is how I see the situation:

I provide a package that is affected by this bug. I don’t use angular-cli in my projects, but my users do. They blame me for a broken product, where in fact the bug is not in my code, but in the optimizer. I had to find this out for my users during painful debugging (it is hard to find a bug in uglified code if the bug is that the code in question is not there, especially if the library that breaks is 6MB in size in already minified form - that’s also why “fixing my library” is not an option - I don’t even know how many code parts have been removed by the setting and I certainly don’t feel like I have to change hundreds of lines of working code when the actual fix is one line of code in the tool that is responsible for breaking my code.)

I researched the problem and found that others were affected by the issue, too. I reported the bug and the issue simply got archived (ignored from my point of view) without being transitioned. The bug report had a complete analysis, repro, and the second time I reported it (and had researched other issues that were basically duplicates) I also included a proposed fix and asked whether and how I could help with a pull-request to fix this issue.

However I was told that basically my code is wrong and it’s bad style and I should simply not be doing it and I should rather spend my time “fixing” all the other packages that suffer from this problem and use a more functional coding style. We were told we should instead stick to some “recommended guidelines for advanced optimizations” if we want to use the angular tool-chain, which -although requested- we have not been given so far. And we were told that this is an important optimization that leads to huge size savings and size savings have a higher priority than handling these “corner cases” where code breaks. I asked for how much they think that this setting is saving, but also never got a response. We showed that these savings are extremely small and that many users would benefit and only very few would suffer from this change, but still they don’t want to even consider our request.

You labeled this as “needs-discussion”. What is it that you feel needs further discussion? What facts are you missing? Given the facts that have been listed here I personally really don’t see what valid arguments one could have for leaving this setting as is. Maybe there are strong facts against that, but I fail to see them; they have not been provided. So if you think that this requires further discussion, I would very kindly ask you or the others to provide a valid argument backed with facts for keeping the setting as is. With the current facts I don’t think it can be “size savings”. And it definitely cannot be “spec conformance” or “ease of use”. I can only see a lot of room for improvements, here.

I am definitely not trying to be hostile; I am trying not to be hostile. I would just like to help the users of my package and of other packages to be able to use the tool of their choice painlessly.

OK, @clydin - now that we’ve seen how “important” that further optimization flag is (it accounts for a whopping 0.7% size reduction and that size reduction also consists of the code that has been removed and is now broken, and should have never been removed) and now that we’ve had more people saying that they are affected by this issue, would you still not consider a pull request that changes the default to a less criticial value?

If no, please back up your statement why saving 0.7% is a more important improvement over risking broken code.

@shileen has shown that the savings in their project and I am sure that almost every developer would rather prefer code that is less likely to break in the optimized version and behaves the same as the unoptimized version vs. code that is 0.7% smaller but might be broken and is extremely difficult to debug.

@filipesilva - would you still say that you feel that for situations like in Shileen’s it is the right way to disable the optimizer completely rather than have the default changed or at least make this configurable? Given the above facts, I personally would be changing the default and not even make it configurable. If someone wants to have this 0.7% size savings and risk that their code breaks, they should be fine with a post install step that or a second manual optimization run that removes the unwanted code.

Please consider accepting a pull-request to change this setting, even without a list of projects “that were not willing, or unable, to update their code to follow the recommended guideline”, whatever these unknown “guideline” may be?

@fbernhard This somehow didn’t work for me. The replace library was not working as expected. So if someone needs another workaround, I use patch-package tool to patch the library. Here’s a good example of how to use that.

As mentioned previously, it is opt-out. Advanced optimizations (i.e., build optimizer) can be disabled if desired. Considering the small number of cases of this actually causing an issue (and not defects in uglify itself) and the ability to manually modify the setting if absolutely required via a post install step, there are no plans to change this.

Please note that this statement “Any code that has side effects in a property access is asking for trouble” was in reference to users of a library that decides to take this approach as it is incredibly unexpected to have a property access perform additional side effects in a production scenario. Further, functional versus imperative is not a relevant distinction. Essentially every object-oriented language’s best practices recommends not causing side effects within get accessors.

For what is it an important optimization to remove code that has side-effects? How great are the gains that you get from this “optimization”? Are they truely worth it asking for troubles? Can you please elaborate on the importance of this optimization.

“Any code that has side effects in a property access is asking for trouble” - sorry, but this statement is just ignorant. That’s not true in the general case. Any compliant JavaScript engine and any code mangling tool that is worth its name can deal with this case properly and it is a bug if it cannot. Any code that does this and which will be “optimized” by this tool is asking for trouble, for sure. There are a great number of very valid use-cases for this. If you don’t like side-effects, then don’t use JavaScript but some functional language. However as a tool author you should never force your preferences onto all third party library authors.

It is fine to restrict code in Angular or in code that you have control over, but if you want to support JavaScript, then you should at least make it possible to configure the “optimization” process not to break code. UglifyJS does have this option, why don’t you just turn it on?

“Never break code” may be impossible, but always breaking code that is perfectly valid and part of a very widely used language construct is not acceptable. Make this an opt-in, or at the very least an opt-out, but don’t leave it broken for the rest of the internet.