Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

DCE Idea: Unused static methods #1709

Closed
samouri opened this issue Oct 25, 2021 · 3 comments
Closed

DCE Idea: Unused static methods #1709

samouri opened this issue Oct 25, 2021 · 3 comments

Comments

@samouri
Copy link

samouri commented Oct 25, 2021

summary

Given an input:

class Foo {
  static bar() {console.log('bar')}
  static qux() {console.log('qux')}
}

Foo.qux();

Is it possible to statically analyze the code and remove the definition of Foo.bar?
Closure Compiler is able to reduce this to single console.log statement, whereas other bundlers like esbuild, rollup, and parcel retain the full definition of Foo.


Note: I did find a thorough comment from earlier (#771 (comment)), but was wondering if the introduction of static changes feasability

@evanw
Copy link
Owner

evanw commented Oct 26, 2021

That comment still applies. In JavaScript, the class body of Foo isn't just a namespace of exported functions the way an ES module is. It's an actual object that exists in the VM at run-time, and all of its properties can be accessed dynamically at run-time. For example:

class Foo {
  static bar() { console.log('bar') }
  static qux() { console.log('qux') }
}

Foo.qux();
new Foo().constructor.bar();

To be able to safely remove bar in this example, you would need a powerful analysis which proves that it's impossible for any instance of Foo to be passed to any code that could potentially dynamically access the constructor property and potentially call methods on it. That requires type inference to track which variables, arrays, and objects in the program could possibly contain instances of Foo, which requires a sound type system for this to be a safe transformation, which runs into type inference precision issues due to the dynamic nature of JavaScript and the possibility of additional code being loaded at run-time, which can be improved by manually-specified type annotations. But at that point you are inventing your own typed version of JavaScript. It's possible to simplify the problem by considering accessing bar in any way other than calling Foo.bar() to be "undefined behavior" and potentially behave differently when the optimizer is invoked. However, then you have an optimizer which is unreliable and unsafe.

Personally I think the reasonable approaches to this problem are to either a) just use plain functions instead of static methods, which are trivially analyzable or b) just use another programming language that doesn't allow you to access static methods indirectly like this in the first place, which also makes this trivially analyzable. Google Closure Compiler's approach of creating a custom type system on top of JavaScript with its own semantics, restrictions, and undefined behavior and then creating an optimizer for it is admirable, but extremely complex and unsafe, which puts it both out of scope and against esbuild's ideals, respectively. If you want this type of thing in your JavaScript code then I recommend that you just use Google Closure Compiler. The custom type system and automatic type inference features required to implement this feature safely is not a complexity threshold that I think esbuild should cross.

@evanw
Copy link
Owner

evanw commented Oct 27, 2021

Closing this issue as this is out of scope.

@evanw evanw closed this as completed Oct 27, 2021
@samouri
Copy link
Author

samouri commented Oct 27, 2021

Thank you for the highly detailed answer @evanw! Just in case others run into this issue, I'll include the workaround I've discovered.

If the only intention of the class is to use it as a namespace for functions, then converting to a plain object provides a similar API surface that terser can DCE. For example, qux will be stripped in this case when run through terser:

const Foo = {
  bar: () => console.log('bar'),
  qux: () => console.log('qux'),
}

Foo.bar();

I did find a couple of cases that unexpectedly deopt, so if using this workaround you'll need to be careful of those (self-referencing object, method shorthand, etc)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

2 participants