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

Suggestion: minification #8

Open
RyanCavanaugh opened this issue Jul 15, 2014 · 178 comments
Open

Suggestion: minification #8

RyanCavanaugh opened this issue Jul 15, 2014 · 178 comments

Comments

@RyanCavanaugh
Copy link
Member

@RyanCavanaugh RyanCavanaugh commented Jul 15, 2014

TypeScript should support emitting minified JavaScript.

There are several different things we could support:

  1. Just remove whitespace
  2. Minify unobservable identifiers
  3. Remove provably dead code
  4. Whole-program minification (i.e. closure compiler)
  5. (Others?)
@chaser92
Copy link

@chaser92 chaser92 commented Aug 7, 2014

I think that this isn't the best idea - TypeScript should better do what it's best at, one tool should serve one purpose. There are a lot of great minifiers out there.

@NoelAbrahams
Copy link

@NoelAbrahams NoelAbrahams commented Aug 7, 2014

@chaser92, this request is to minify TypeScript code, not JavaScript code. A minifier that uses the information (primarily information on access modifiers) available to the TypeScript compiler will be much more efficient than any JavaScript minifier out there.

I am personally very eager to see this implemented.

@RyanCavanaugh
Copy link
Member Author

@RyanCavanaugh RyanCavanaugh commented Aug 7, 2014

Motivating examples of things that TypeScript could minify but an external minifier could not would be useful. Things like closure and uglify do a really good job already; we'd have to have evidence we could make real improvements over them to justify spending time on it.

@NoelAbrahams
Copy link

@NoelAbrahams NoelAbrahams commented Aug 11, 2014

The following TypeScript code

class Greeter {

    private greeting: string;

    constructor (message: string) {
        this.greeting = message;
    }

    public greet() {
        console.log(this.getMessage());
    }

    private getMessage() {
        return "Hello, " + this.greeting;
    }
}

Can be compiled into JavaScript that when run through an external minifier results in the following:

var Greeter = (

    function () {

        function a(b) {
            this.greeting = b
        }

        a.prototype.greet = function () {
            console.log(this.getMessage())
        };

        a.prototype.getMessage = function () {
            return "Hello, " + this.greeting
        };

        return a
    }
)();

Problems:

  • The private field greeting has not been minified.
  • The private method 'getMessage` has not been minified.
  • The type name "Greeter" has been mangled into "a". This breaks code such as (/function (.{1,})\(/).exec((instance).constructor.toString()) for obtaining the type name at runtime.
  • The constructor parameter "message" has been mangled into "b". This breaks code that attempt to perform dependency injection by parsing constructor arguments.

In summary, even in just this simple snippet of code TypeScript can improve on an external minifier, both by mangling names that should have been minified as well as optionally leaving names untouched.

@RyanCavanaugh
Copy link
Member Author

@RyanCavanaugh RyanCavanaugh commented Aug 11, 2014

If I use the Closure Compiler, it goes from code like this:

var Greeter = (function () {
    function Greeter(message) {
        this.greeting = message;
    }
    Greeter.prototype.greet = function () {
        console.log(this.getMessage());
    };

    Greeter.prototype.getMessage = function () {
        return "Hello, " + this.greeting;
    };
    return Greeter;
})();


var x = new Greeter();
x.greet();
console.log(x.getMessage());

to this:

var b = new (function() {
  function a(a) {
    this.c = a;
  }
  a.prototype.b = function() {
    console.log(this.a());
  };
  a.prototype.a = function() {
    return "Hello, " + this.c;
  };
  return a;
}());
b.b();
console.log(b.a());

Here, the greeting field and getMessage names have been correctly minified. The "over-minification" of other variables leads to my next discussion point on this.

Some people think class names, property names, parameter names, local names, etc are meaningful runtime metadata, other people think they're not. Some other people think only some of their property names, parameter names, local names, and so on are things that should be minified. Among those people, there's disagreement over how those names should be specified (globally? locally? in the code? in a config file? on a commandline?). Nearly everyone believes that their set of rules is the only one that makes sense.

The only path forward that could beat existing minifiers also involves a ton of configuration for what the allowed set of minifications even is; implementing and testing all these rules would be very expensive. It's a lot of investment for what is probably a few percent improvement (especially after gzipping) over the state of the art here. That's why we want to see compelling examples for where only the TypeScript compiler could minify and do a meaningfully better job than any existing tool.

@NoelAbrahams
Copy link

@NoelAbrahams NoelAbrahams commented Aug 12, 2014

@RyanCavanaugh,

Regarding the under-minification problem, it looks like the code in your post was minified using the "advanced" option of the closure "compiler". And yes, that appears to solve the problem in this simple case. However, consider this slightly more involved example:

class Greeter {

    public greet() {
        console.log('greet');
    }
}

function createInstance<TType>(name:string): TType {

    return new window[name]();
}

var x = createInstance<Greeter>('Greeter');
x.greet();

Basically, we have introduced a class factory function. The closure compiler, with the advanced option, minifies this to

(function() {
  function a() {
  }
  a.prototype.a = function() {
    console.log("greet");
  };
  return a;
})();
(new window.Greeter).a();

Clearly this is going to fail at runtime. The solution is to export the "Greeter" symbol:

window["Greeter"] = Greeter;

Seems simple enough. But in large projects with hundreds of classes, this is not trivial. TypeScript understands this code better, because it knows that function Greeter is a class name.

More importantly, the problem I have with the closure compiler is that it requires the entire code-base to be minified in one go. This is not feasible in large projects, where there is separation between library code and client code. In this situation it is necessary to declare externs, which ultimately results in maintaining one's public API in duplicate.

With regard to the second issue that was raised, namely the problem of configuration, that, I guess, is an implementation detail: if there is demand for configuration then that would have to be dealt with. But it seems strange to decline an entire issue on that basis.

A possible halfway solution would be for TypeScript to provide an option to generate closure-style extern files or to export symbols (class names).

@danquirk
Copy link
Member

@danquirk danquirk commented Aug 12, 2014

The configuration is not just an implementation detail. The fact that we can go back and forth all day with different code examples that need differing amounts of minification is proof of the need for either a) very simple minification algorithms b) extremely customizable minification options. You've already identified multiple issues that absolutely demand customization to work at all.

In addition, we're still talking about this nebulous concept of 'the TypeScript compiler understands the code better so it should just do this example correctly' (for some definition of correctly). There's still very little here that is actually stating proposed solutions to classes of problems which you could put a configuration over the top of.

@NoelAbrahams
Copy link

@NoelAbrahams NoelAbrahams commented Aug 17, 2014

To summarise the discussion so far:

  • The JavaScript generated by TypeScript is not optimal in terms of providing that output as input to a simple minifier. This is largely due to private methods going on the prototype. A developer wanting to write code that is optimal for a minifier would have written those private methods as simple functions within the class closure.
  • This problem can be solved by running the output through an advanced minifier (for example the closure compiler with the "advanced" option).
  • Unfortunately the closure compiler requires all symbols to be within sight of the minification run. If they are not then those symbols must either be exported (added using bracket notation) or an externs file must be created, declaring the symbols that should be preserved.
  • User preferences for minification can be diverse.
  • The TypeScript compiler has a superior information set with which to perform minification than a JavaScript minifier; this information set includes having access to the following modifiers "class", "private", "public", "constructor" and the API of external libraries through their declarations files.
  • The biggest advantage that a TypeScript compiler has over ordinary minifiers is being able to safely perform minification on any subset of the code-base. By "safely" I mean that, firstly, exported classes and their public API can be preserved, and, secondly, calls made from within a class into external libraries can also be preserved with recourse to the information in their respective declarations files.

In the light of the above, I have three basic proposals listed in order of preference:

A. A fully functional minifier provided by TypeScript.
We provide two compiler options

  • --minify-simple - Preserves class names, constructor signature...
  • --minify-advanced - Performs a more aggressive minification.

In both cases, private fields and methods would always be minified.

B. TypeScript generates externs files for providing to the Closure Compiler if specified.

C. TypeScript only minifies private fields and methods if specified.

It is conceivable that with option A there will be many people who would like a halfway solution between "simple" and "advanced" minification, e.g. preserve class names but minify constructor signature. In this instance, it may be possible for those concerned to run the output generated by TypeScript through a third-party minifier that provides more specific options.

@ghost
Copy link

@ghost ghost commented Aug 19, 2014

I like your B suggestion (TypeScript generates externs files for providing to the Closure Compiler). Not sure how it works but I like the sound of it when considering the points that everyone else is making.

@bryanerayner
Copy link

@bryanerayner bryanerayner commented Aug 23, 2014

TL;DR
Perhaps Data-Annotations could be added to Typescript's syntax to provide developers with an easy way to tell the compiler what should, and should not be overly minified.

I have been writing a large Angular app using Typescript. I've been painfully watching the size of the JS grow and grow, and would personally LOVE to have some advanced compilation support that only Typescript can provide. IMO Typescript can do an incredibly good job at minifying code, better than closure because you're not limited to comments for specifying compiler directives.

For an Angular app, there needs to be a high level of granularity of what functions and properties are minified (a la Closure Compiler's advanced mode) and what properties should be left as-is. They are referenced in the HTML and oftentimes used by Angular to correctly link the DOM to JS. You'd need a solution on an item by item basis in order to accomplish this in a way that would make it easy to use.

For example, much in an Anuglar directive can be truly 'Javascript Only', and I would like advanced minification on it. However, there are some variables that need to be exposed to HTML, outside of the javascript engine.

If we were able to use something similar to a C# data-annotation on class properties, that would be a much better method than Closure compiler's recommendations. I've seen a lot of Javascript written for the Closure compiler that uses array notation to reference unminified properties - This is a pain to write. Any time you reference a property by a string in Javascript, it just feels muddy to me.

I've been using Newtonsoft.Json on a dot Net backend. We directly serialize our business logic models to the client side - Of course, we want to keep some things hidden from JSON serialization. For those without a C# background, a data annotation looks like this:

Imports Newtonsoft.Json

class Klass
{
    [JsonIgnore]
    public string[] SomethingVeryLarge;

    public string SomethingMoreManageable;
}

The [JsonIgnore] data annotation instructs Json.Net to overlook this property when parsing an instance of Klass.

Having something like data-annotations could provide the compiler with a really good system of flags, that could be used for this advanced minification support, or other compiler features. I could see this syntax eventually being usable Typescript programs to further extend the language.

@NoelAbrahams
Copy link

@NoelAbrahams NoelAbrahams commented Aug 24, 2014

@bryanerayner,

there are some variables that need to be exposed to HTML, outside of the javascript engine.

Yes, that's a relevant point as well. This is also the case when using KnockoutJS, for example:

class ViewModel {

  [minifyIgnore]
  public foo = ko.observable('bar');
}

because foo is referenced in the HTML:

<div data-bind="text:foo" />
@eggers
Copy link

@eggers eggers commented Sep 11, 2014

I would love a typescript aware minifier. Also, because I've had some issues with ng-min not working very well on tsc generated javascript. (I haven't tried ng-annotate yet)

@WanderWang
Copy link

@WanderWang WanderWang commented Sep 16, 2014

I Agree with @RyanCavanaugh
I'm working for a Open Source HTML5 Game Framework called EGRET
Many of our developers thinks that our game.min.js should be smaller and smaller .
Now we compress our game.min.js with Google Closure Compiler SIMPLE_OPTIMIZATION , there are some reason we abandon ADVANCED_OPTIMIZATION

  • It's very hard to use ADVANCED in a complex project with Zero bug . We had to test the whole the test case carefully again . When we turned to SIMPLE mode , it has too many private long-named field not be optimized , C is a useful solution with SIMPLE mode
  • We should support our customer a lib.min.js and there are many public api inside . But ADVANCED_OPTIMIZATION delete almost all the apis because it thinks that the apis not be used . To solve this problem , we should provide a extern-file to Closure Compiler , but we have to many API . B solution with ADVANCED mode is a good idea , because of tsc can generate the extern files very easy ( such as .d.ts )

So, I hope both B and C could be added to tsc .
By the way ,forgot A ,please ^_^

@mirhagk
Copy link

@mirhagk mirhagk commented Sep 29, 2014

I'm noticing that the problem with using Closure is that there is information about visibility that TypeScript is aware of that Closure can't know.

This is why the private members stick around when using simple with Closure. Using advanced can cause headaches.

TypeScript can make it easier to use advanced with option B. It could also potentially make simple optimizations with closure better if it could communicate to closure which methods/variables are private. It could emit JSDoc comments to decorate fields with @private.

Currently the Google Closure compiler doesn't use these hints for anything useful, but providing that information for it doesn't hurt (since it removes comments) and it gives minifiers more information to optimize with.

@antoinerousseau
Copy link

@antoinerousseau antoinerousseau commented Jan 22, 2015

If option A is fully efficient, I don't see why you would need B or C!

I would love to have option A's --minify-advanced!

@mirhagk
Copy link

@mirhagk mirhagk commented Jan 22, 2015

I think the issue is that the compiler shouldn't necessarily be responsible for minification. I would much rather see all the information that TypeScript knows be emitted as jsdoc comments. That way JavaScript tools can utilize that information without needing to have a TypeScript parser.

@ghost
Copy link

@ghost ghost commented Jan 22, 2015

Don't compilers generally have a switch to optimize for file size? Why not this one?

@mirhagk
Copy link

@mirhagk mirhagk commented Jan 22, 2015

Compilers don't usually have a goal of near 1:1 mapping with source.

The point is that generating the resulting code is completely different with optimizing for file size (and other optimizations) than what it currently produces. I'd suggest a separate tool is created for an optimizing compiler (one that optimizes for either file size or performance), that makes use of the API tsc offers. I was planning on experimenting with the tsc compiler API so I may do an experiment in producing minified output.

@ghost
Copy link

@ghost ghost commented Jan 22, 2015

The goal of the TypeScript compiler could be near 1:1 mapping of source unless the /min switch is set, in which case the goal would be to minimize it to the hilt without changing its exports.

@mirhagk
Copy link

@mirhagk mirhagk commented Jan 22, 2015

Yes that's true, but then the back-ends are basically completely separate. I'd recommend implementing it as a separate module/project that uses the API, then if it does prove very useful it can be evaluated to be merged as part of the tsc command line.

@joelgwebber
Copy link

@joelgwebber joelgwebber commented Mar 13, 2015

Meta: Is this the canonical issue tracking the proposal to create a path to an optimizing backend (be it built-in or via Closure annotations)? It appears to be at a glance, but if I'm missing a more appropriate spot, I'd appreciate a pointer to it.

I'd just like to throw in my 2¢ in support of an optimizing backend. My company's using Typescript very heavily, and while using Closure's basic optimization mode on the output helps to a certain extent, it's quite obvious that we can do a lot better. Code size may not matter to everyone, but it should -- it's not just about over-the-wire size, which is (mostly) mitigated by gzip, but about memory use, parse time, and runtime performance (yes, you can still achieve significant improvement through static optimization). There's a reason that Google invested heavily in optimization for both the Closure and GWT stacks (full disclosure -- I worked a lot on GWT and to a limited extent on Closure when I was there).

Just to be clear, this is what I mean when I suggest that we can do a lot better than just dropping whitespace and obfuscating local identifiers:

function rAb(a,b){if(!b.ib.a){Sae(a.a.a.a.a.d,(Klf(),new Nwf(b)));bOe(a.a.a.a.a.i,a.a.a.a.a.i.c)}}
function Cqb(a,b){a.f=b;if(!b.yb){a.a.we(new Aqb(a.b,a.f,null));return}wqb(a.d,b.Fb,new Iqb(a))}
function Bqb(a,b){var c;a.b=b;c=b.g;if(c.fh()){Cqb(a,b);return}vae(a.c,c.Br(0).Fb,false,new bbe(new Gqb(a)))}
function Pfd(a,b,c){var d;d=new Ufd(a);Bgd(a.a,d);Agd(a.a,b,c);dgd(a.b,new Wfd(a));Jpb(a.b,new Yfd(a));Qfd(a)}
function Ppd(a){var b;b=new qid(Dte(AFd(a.a)),bue(AFd(a.a)),aue(AFd(a.a)),Opd(a),knc(XEd(a.a)));return b}
function $pd(a){var b;b=new kmd(NXf(QFd(a.a)),(new rnd,KFd(a.a),new ndf),new hid(pKc($Ed(a.a))));return b}
function Rnd(a){var b;Sgf(a.s);if(a.o.S){return null}b=a.U.vg();return !!kfi(b).length&&!u_(b)?b:null}
function Pnd(a){Sgf(a.s);if(a.o.S){return nt(),mt}if(a.N.e.f.a){return nt(),mt}else{Sgf(a.M.e.f.a);return nt(),kt}}
function Gld(a){if(!a.j||!ZM(a.d.b,(Jw(),Qv))){return scf((Yci(),Yci(),Wci))}return icf(a.n,new Jld)}
function Mkd(a){a.a.wk((RJf(),KJf));wbb(a.a,eui);a.a.Oe(true);Fbb(a.a,new Ukd(a),BRf?BRf:(BRf=new wQf))}
function Jhd(a,b){var c,d;d=Bae(a.c,b);c=Lld(Ypd(a.a.a),b,d);return Nhd(new Ohd(a),(Ngf(c.d),new Ild(c,true)))}

The above is a random chunk of GWT output from a Google app. The details don't matter -- the point being that aggressive optimization can dramatically reduce output size, make a non-trivial difference in parse time and runtime performance, and even amplify gzip compression by allocating identifiers in such a way as to reduce input entropy.

As has been pointed out earlier on this thread, there are two obvious routes we can take:

  • Build an optimizing backend into tsc.
  • Add an option for generating Closure jsdoc annotations.

I don't have a strong preference -- as long as it's possible to get good output, I don't care much how we get there. The output example above came from the GWT compiler, but Closure achieves similar results. Michael Bolin (who did a lot of the work on Closure at Google) created a proof-of-concept (http://bolinfest.com/typescript/) a couple of years ago, but didn't take it much further than that. It's not an entirely trivial exercise, because of the impedance mismatch between Typescript and Closure's inheritance mechanism in particular, but it doesn't seem like brain surgery either.

The hardest design problem, as I see it, is dealing with exposed vs. optimizable symbols. Closure has annotations for dealing with "exporting" symbols, and Typescript would need something similar to make the optimizer useful. There are also important edge cases like dealing with externally defined objects (both importing third-party libraries, and dealing with the output of APIs like JSON.parse). The compiler must know about these things if it is to avoid breaking the output with an aggressive optimizer.

I think it would be fairly easy to rally a few people to work on an optimizing backend for Typescript, but only if the team is bought into the idea. Trying to bolt such a thing onto the existing compiler, without the ability to tweak the compiler and language a bit, is probably a fool's errand. So I'd greatly appreciate any indication from the team as to their disposition on the subject.

@NoelAbrahams
Copy link

@NoelAbrahams NoelAbrahams commented Mar 13, 2015

I really like the "random chunk of GWT output from a Google app". In addition to the benefits mentioned above, another objective is protection of intellectual property through obfuscation. If anybody has tried stepping through the code in Google Maps then they will know the protection that aggressive minification can provide in this regard.

The annotation (for excluding minificaton) is not only relevant for externally defined objects, but also relevant when properties of JavaScript objects are bound to elements in the HTML.

@joelgwebber
Copy link

@joelgwebber joelgwebber commented Mar 13, 2015

I decided not to say anything about obfuscation for the purpose of, well... "obfuscation", at least partially because I know that tends to raise a lot of hackles in the web world :) A bit more seriously, I'm less concerned about it simply because I've spent so much time reverse-engineering highly-obfuscated Javascript (e.g., http://www.j15r.com/blog/2005/02/09/Mapping_Google) that obfuscation only feels like a speed bump. But hey, a big speed bump can still be useful.

Regarding annotation, right -- I forgot to mention that runtime-provided types are equivalent to external code. The (small, but important) distinction being that you tend to have relatively stable and available IDL for the former, whereas the latter's a bit of the wild west. If you have type information (e.g., via DefinitelyTyped), you're good to go, but if not you have a problem. Personally, I'd be fine with requiring explicit type information for all external libraries as a precondition for aggressive optimization, but I'm not sure that's a majority opinion.

FWIW, Closure basically takes that approach. If you don't have type annotations for external code, Turing only knows what it will do under advanced optimization (actually, I don't even think Turing knows, because it's probably undecidable, and the compiler certainly can't tell you). With GWT it was a bit easier, because you needed explicit (lightweight, optimized away) Java interface wrappers to call into Javascript in the first place. So the compiler only has an aggressive mode, because it can prove that the output won't be broken (as long as your Javascript methods don't "cheat").

Requiring explicit types also works for dealing with parsed JSON (our rather large body of Typescript uses interfaces for all parsed values; otherwise we'd be breaking things left and right). I believe Closure allows for this, but the traditional method was to require string accessor syntax for any properties that weren't defined by the compiler (e.g., value = parsedThing["someField"]). This is a pretty foul hack that I believe goes unchecked by the compiler, though, and I wouldn't be in favor of doing anything that nasty in Typescript. After all, that's one of the great benefits of making interface definitions an order-of-magnitude less verbose than in Closure.

@CrimsonCodes0
Copy link

@CrimsonCodes0 CrimsonCodes0 commented Oct 13, 2020

AssemblyScript already emits the most minified, non-human-readable code that a TypeScript compiler should ever need to, and from what I hear, It's well-optimized too!

No, it's not TypeScript, it's a subset, but that might change as time goes on.

By minifying TSC output, the TS developers essentially encourage the misusage of JavaScript as a compilation target.
At minimum, one should target asm.js, but it has been deprecated in favor of Wasm.

This is probably worth closing and saying that TSC removes bugs and headaches, not whitespace; that is already a very important feature that we are all grateful for ❤️.

The TypeScript development team should focus on TypeScript.
Minification, optimization, and related issues should be taken on by the community, by using the syntax and type output API that TSC already offers.

@aminya
Copy link

@aminya aminya commented Nov 14, 2020

AssemblyScript already emits the most minified, non-human-readable code that a TypeScript compiler should ever need to, and from what I hear, It's well-optimized too!

I really like the way you look into this issue. The JS target of AssemblyScript uses types to produce the runtime optimized code.

For example,

let x: i32 = foo()

is compiled to the following which hints V8 to optimize this code for integer types instead of floating-point numbers.

let x = foo() | 0

Feeding the produced code to Terser (or other minifies) can further reduce the size.

I actually raised this once in one of the discussions with the core team of AssemblyScript. For me, the reason that the JS target is useful is that DOM API is still missing in WASM, and so WASM does not completely replace JavaScript yet.

There are still some things to improve in the JS target. For example, I had some concerns with reusing the modules produced this way (e.g. if the modules are ES6 compliment, etc), or if all the generated code (like globals) are needed for JS target.

cc: @MaxGraey @torch2424

@CrimsonCodes0
Copy link

@CrimsonCodes0 CrimsonCodes0 commented Nov 21, 2020

@aminya

...The JS target of AssemblyScript uses types to produce the runtime optimized code.

is compiled to the following which hints V8 to optimize this code for integer types instead of floating-point numbers.

let x = foo() | 0

Not only does that hint ES implementations/runtimes to use 32-bit integers, but it also forces them to. For example, if foo were an imported JS function, that would coerce the result into an int.
This also ensures that the runtime semantics of AS are preserved. Let's say that the JS function foo was defined as:

const foo = () => 0xffff * 0x10000;

This would result in an overflow, but now the AS generated JS would have the same behavior as the Wasm.

There are more interesting cases, such as i64, how do they handle that? 64-bit integers would have to represented as JS BigInts in order to maintain precision.

I actually raised this once in one of the discussions with the core team of AssemblyScript. For me, the reason that the JS target is useful is that DOM API is still missing in WASM, and so WASM does not completely replace JavaScript yet.

Of course not, Wasm doesn't replace JS, at least not yet.
It was originally intended that authors would write parts of their code in normal JS, and performance-critical parts in asm.js.
Now Wasm replaces asm.js, but they still fill the same role.

But, with the newer interfacing types proposals and such, it's not infeasible to imagine a Wasm controlled front-end web.
When Wasm has native access to WebIDL types, you can expect quite a change.

But in the meanwhile, someone at AS should try to mimic Rust Wasm Bindgen's ABI. It gives some access over JS, while the control flow primarily still remains in Wasm. Differentiating between JS objects and Wasm objects may get complicated, they may need to introduce a type-level API to allow this type of functionality.

@aadityasivaS
Copy link

@aadityasivaS aadityasivaS commented Dec 21, 2020

@RyanCavanaugh 👋 Hi there I am working on a command-line wrapper for typescript which will minify the code in the output file with the uglifyjs library I will release it soon.

@aadityasivaS
Copy link

@aadityasivaS aadityasivaS commented Dec 21, 2020

TypeScript should support emitting minified JavaScript.

There are several different things we could support:

  1. Just remove whitespace
  2. Minify unobservable identifiers
  3. Remove provably dead code
  4. Whole-program minification (i.e. closure compiler)
  5. (Others?)

Okay I got it

@JustFly1984
Copy link

@JustFly1984 JustFly1984 commented Dec 21, 2020

@aadityasivaS can you please consider terser instead of uglifyjs?

@aadityasivaS
Copy link

@aadityasivaS aadityasivaS commented Dec 21, 2020

@aadityasivaS can you please consider terser instead of uglifyjs?

okay

@manigandham
Copy link

@manigandham manigandham commented Dec 21, 2020

@RyanCavanaugh 👋 Hi there I am working on a command-line wrapper for typescript which will minify the code in the output file with the uglifyjs library I will release it soon.

To be clear, the output file is javascript and can already be processed by many different tools. This issue is about minifying during the Typescript compilation itself using information that only Typescript has.

Do you have some way of capturing and using the Typescript metadata with the other minifier tooling? Otherwise just running uglify/terser on the output is not the solution.

@aadityasivaS
Copy link

@aadityasivaS aadityasivaS commented Dec 21, 2020

@RyanCavanaugh 👋 Hi there I am working on a command-line wrapper for typescript which will minify the code in the output file with the uglifyjs library I will release it soon.

To be clear, the output file is javascript and can already be processed by many different tools. This issue is about minifying during the Typescript compilation itself using information that only Typescript has.

Do you have some way of capturing, transferring, and using the Typescript metadata with the other minifier tooling? Otherwise just running uglify/terser on the output is not the solution.

oh okay
anyway thanks

@CrimsonCodes0
Copy link

@CrimsonCodes0 CrimsonCodes0 commented Jan 7, 2021

Whole-program minification (i.e. closure compiler)

Well... if you really want a reason to give up, here I go.

Let's use everyone's favorite function: a Fibonacci function, as an example!

Take this JavaScript Fibonacci implementation:

const sqrt5 = 5.0 ** 0.5;
const phi = (sqrt5 + 1.0) / 2.0;

export const fib = n => {
	const phi_pow = phi ** n;

	const fraction = -phi ** -n;

	return Math.floor((phi_pow + fraction) / sqrt5);
};

Maximally optimizing this would be illegal.
This is a whole lot of instructions: multiple exponentiation operations, assignment ops, division ops, room for two ToPrimitive ops, subtraction, property lookups, function calls, addition ops, variable lookups, scope lookups, etc.

Take this input:

fib({
    [Symbol.toPrimitive]: () => {
        // ...
    }
});

This will cause these two lines to call the dreaded toPrimitive ops:

const phi_pow = phi ** n;

const fraction = -phi ** -n;

And that, is how you cause deoptimization to happen in any JS runtime :)

Honestly, I don't even want to put in the mental effort to optimize that.

Let's take the TS version:

type f64 = number;

const sqrt5: f64 = 5.0 ** 0.5;
const phi: f64 = (sqrt5 + 1.0) / 2.0;

export const fib = (n: f64): f64 => {
	const phi_pow: f64 = phi ** n;

	const fraction: f64 = -phi ** -n;

	return Math.floor((phi_pow + fraction) / sqrt5);
};

To start, we can perform constant folding on sqrt5 and phi:

const sqrt5: f64 = 2.23606797749979;
const phi: f64 = 1.618033988749895;

export const fib = (n: f64): f64 => {
	const phi_pow: f64 = phi ** n;

	const fraction: f64 = -phi ** -n;

	return Math.floor((phi_pow + fraction) / sqrt5);
};

Cool, let's inline those:

export const fib = (n: f64): f64 => {
	const phi_pow: f64 = 1.618033988749895 ** n;

	const fraction: f64 = -1.618033988749895 ** -n;

	return Math.floor((phi_pow + fraction) / 2.23606797749979);
};

Because the parameter n should always be a number, we can forget about those deopts that I had mentioned earlier, allowing us to do something tricky, like... rewriting those exponentiation operations:

const fraction: f64 = 1.0 / -1.618033988749895 ** n;

then move around the negation

const fraction: f64 = -1.0 / 1.618033988749895 ** n;

and then we can de-duplicate the entire exponentiation operation and variable lookup!

const fraction: f64 = -1.0 / phi_pow;

You know what? I'll skip the rest, no one cares about the process:

Here's the best that an advanced optimizing compiler could do that would be legal with TypeScript (note C compilers aren't even this good):

export const fib = (n: f64): f64 => Math.floor(
	1.0 / (n = 1.618033988749895 ** n) + n / 2.23606797749979
);

Go find me the minifier that can out perform that, I dare you.
Closure compiler can't even do that with advanced optimizations.

Yet, it's not the best that it could be, performance-wise.
We have an unnecessary lookup on the Math object.

Hoisting it, we would get this:

type f64 = number;

const { floor } = Math;

export const fib = (n: f64): f64 => floor(
	1.0 / (n = 1.618033988749895 ** n) + n / 2.23606797749979
);

Does anyone want to benchmark that func and compare it to AssemblyScript's hello world fib func to prove that plain TypeScript out-performs AssemblyScript?

That is my hand-optimized code for you, do you know how hard it would be to do this for every single line of code across my codebase?
Stripping whitespace, the size of the text is 93 bytes. Equivalent Wasm would be 132 bytes, and most of it is just the Wasm header bytes. 39 byte difference; imho, it's not even worth it.

Of course, this is a trivial example, compiling entire web applications show much more major differences, but in the end, this is mistargeting JS as a compilation target, not to even talk about TS's any type and such, that create loopholes in the type system, effectively making it as good as JS anyway. TypeScript is not the right language to be minified+optimized, and JavaScript is a mediocre output target at best.

Allow any optimizations, and everyone will have different expectations for the degree that it should be optimized, ask me and I want that ^ level of optimization.

@MaxGraey
Copy link

@MaxGraey MaxGraey commented Jan 7, 2021

Because the parameter n should always be a number, we can forget about those deopts that I had mentioned earlier, let's do something tricky, like... rewriting those exponentiation operations:

Btw rewriting C ** -y -> 1 / (C ** y) is illegal in general and will cause to precision lost. Only if C is power of two like 2.0, 4.0, 16.0 ... it may be possible but I'm not sure even in this case. That's why Closure Compiler, Prepack or LLVM / GCC never rewrite this as you proposed.

@CrimsonCodes0
Copy link

@CrimsonCodes0 CrimsonCodes0 commented Jan 7, 2021

Btw rewriting C ** -y -> 1 / (C ** y) is illegal in general and will cause to precision lost. Only if C is power of two like 2.0, 4.0, 16.0 ... it may be possible but I'm not sure even in this case. That's why Closure Compiler, Pprepack or LLVM / GCC newer rewrite this as you proposed.

Just to confirm, that "newer" was meant to be "never," right?

These are very valid concerns, no one wants to debug their code's mysterious precision loss, but it can help in very, very tightly executing code and reduce code size nonetheless, therefore we would have to put this under the suggested:

  1. (Others?)

it appears that we would need a --fast-math flag or something of the sort, further complicating TSC.

@MaxGraey
Copy link

@MaxGraey MaxGraey commented Jan 7, 2021

Just to confirm, that "newer" was meant to be "never," right?

Yeah, just typo

@aminya
Copy link

@aminya aminya commented Jan 7, 2021

Because the parameter n should always be a number, we can forget about those deopts that I had mentioned earlier, let's do something tricky, like... rewriting those exponentiation operations:

Btw rewriting C ** -y -> 1 / (C ** y) is illegal in general and will cause to precision lost. Only if C is power of two like 2.0, 4.0, 16.0 ... it may be possible but I'm not sure even in this case. That's why Closure Compiler, Prepack or LLVM / GCC never rewrite this as you proposed.

Don't they do this with -Ofast? -Ofast allows applying the optimizations that may cause precision loss.

-Ofast
Disregard strict standards compliance. -Ofast enables all -O3 optimizations. It also enables optimizations that are not valid for all standard-compliant programs. It turns on -ffast-math, -fallow-store-data-races and the Fortran-specific -fstack-arrays, unless -fmax-stack-var-size is specified, and -fno-protect-paren

@MaxGraey
Copy link

@MaxGraey MaxGraey commented Jan 7, 2021

Yes, but it's not relate to -Oz (which is optimization for size). As I undersatand main goal of this proposal is minification but not optimize especially with loose some precision and potentially broke logic. So C ** -y -> 1 / (C ** y) in general case is not minify code, and may be shrink only if we have similar expression like C ** y in same scope which mean optimize should not only provide peephole optimizations but also provide more high level analysis like cost analysis, sophisticated matching expressions which more smarter than CSE (common sub-expression elimination). In this case TS will become compiler similar to LLVM, Ocaml or Go. I'm not sure this goal of TypeScript)

@aminya
Copy link

@aminya aminya commented Jan 7, 2021

Well, as discussed by other people, regarding the size optimizations, I am not convinced that TypeScript can do anything "much" different than what Terser and Closure Compiler are doing. Especially, when you can override things with any, as, and different projects may use different levels of strictness. You may able to use the types to apply some basic size reduction, but that is not going to be groundbreaking.

That's why I think a mixed target or a js-only target for AssemblyScript is a much better approach for applying "real" optimizations.

The goal of WebAssembly and ASM.js is exactly addressing what everyone wants. "Small and fast" code. For a JS developer, the easiest way to get to that goal is to use AssemblyScript.

@SteffenAnders
Copy link

@SteffenAnders SteffenAnders commented Jan 7, 2021

It would be nice if names of private properties and methods of a class would be minified/mangled by the typescript compiler.
That is something what Terser or UglifyJS cannot do because after transpiling from TS to JS the "public/protected/private" information is lost.

@CrimsonCodes0
Copy link

@CrimsonCodes0 CrimsonCodes0 commented Jan 7, 2021

Don't they do this with -Ofast? -Ofast allows applying the optimizations that may cause precision loss.

-Ofast
Disregard strict standards compliance. -Ofast enables all -O3 optimizations. It also enables optimizations that are not valid for all standard-compliant programs. It turns on -ffast-math, -fallow-store-data-races and the Fortran-specific -fstack-arrays, unless -fmax-stack-var-size is specified, and -fno-protect-paren

Ha, I doubt my Rustc has that type of flag.
Sounds like it might, I'd have to check.
Mathematically, that is, assuming infinite precision, the expressions are equivalent.

Yes, but it's not relate to -Oz (which is optimization for size).

In that particular case, it did shorten the size, but in general, it wouldn't even help.

... and may be shrink only if we have similar expression like C ** y in same scope...

That is precisely why I had done it there, if the expression were not there, I wouldn't have done it at all.

@CrimsonCodes0
Copy link

@CrimsonCodes0 CrimsonCodes0 commented Jan 7, 2021

It would be nice if names of private properties and methods of a class would be minified/mangled by the typescript compiler.
That is something what Terser or UglifyJS cannot do because after transpiling from TS to JS the "public/protected/private" information is lost.

It can't be done anyway, the only reason to use private foo over #foo is to use it outside of the class, e.g. testing.
Can't Terser/UglifyJS minify #foo in a class?

Does this suggest that using TS is actually worse for size than JS?

@MaxGraey
Copy link

@MaxGraey MaxGraey commented Jan 7, 2021

Ha, I doubt my Rustc has that type of flag.
Sounds like it might, I'd have to check.

No, it's can't. See this playground.

https://godbolt.org/z/GvY4Pd

As you can see in both cases (without and and with -ffast-math) LLVM generated two calls

@aminya

This comment was marked as off-topic.

@MaxGraey
Copy link

@MaxGraey MaxGraey commented Jan 7, 2021

C++ uses exp2 instead of pow when Ofast is used instead of O3.

Yes, it's just minor performance optimization. JS Math hasn't Math.exp2(x, y) for example, but anyway it will be more code size than x ** y

@CrimsonCodes0
Copy link

@CrimsonCodes0 CrimsonCodes0 commented Jan 7, 2021

Yes, it's just minor performance optimization. JS Math hasn't Math.exp2(x, y) for example, but anyway it will be more code size than x ** y

In Wasm, these would both be equivalent in size:

local.get $x
local.get $y
call $Math::exp2
local.get $x
local.get $y
call $Math::pow

Which is really just debug info and can be

local.get 0
local.get 1
call 0

Allowing one to optimize Wasm for performance, without size costs, whereas using Math.exp2 in JS would cost two extra bytes per call, with a header size cost:

// const f = Math.exp2;
f(x,y);
x**y;
@MaxGraey
Copy link

@MaxGraey MaxGraey commented Jan 7, 2021

In Wasm, these would both be equivalent in size:

Not really. Wasm hasn't builtin intrinsics for pow and exp2. That's why it call stdlib instead use something like f64.pow. It means code which use two call $Math::pow will be always smaller than code which call one call $Math::pow and one call $Math::exp instead and include implementations for pow and exp2 simultaneously

@RyanCavanaugh
Copy link
Member Author

@RyanCavanaugh RyanCavanaugh commented Jan 7, 2021

This discussion seems to be veering pretty far off-topic; can we reel it in a bit please?

@fabiospampinato
Copy link

@fabiospampinato fabiospampinato commented Jan 13, 2021

There's a significant slice of JS/TS developers that don't care that much about bundle sizes, which contributes to the feeling some people have that JS/TS apps are bloated almost by definition.

It'd be amazing to see a critical player in the ecosystem like TypeScript taking a stance on this, offering an amazing minifier that leveraging the type system is able to deliver best in class minification ratios.

@lonewarrior556
Copy link

@lonewarrior556 lonewarrior556 commented Feb 18, 2021

TypeScript should support emitting minified JavaScript.

There are several different things we could support:

  1. Just remove whitespace
  2. Minify unobservable identifiers
  3. Remove provably dead code
  4. Whole-program minification (i.e. closure compiler)
  5. (Others?)

I would be happy with the easiest thing you could do (number 1)

We have a script that transpiles a separate repo right into our code base using an older version of ts,
Theres is a MD file that says 'generated code do not edit' but it's easy to ignore that and forget.
Having the simplest minification option would work far better at communicating to developers that this code is generated and not meant for editing.

Yes this can be done with additional tools and more lines in a script but a flag like "minifyWhitespace": true would be far nicer.

Ps. Props: If your transpiler didn't make code so readable I wouldn't need this

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

Successfully merging a pull request may close this issue.

None yet