-
Notifications
You must be signed in to change notification settings - Fork 12.6k
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
Reduce overhead and indirection in enum and namespace code generation #54244
Comments
This doesnβt support merging, right? So it would be a breaking change. Given how weird the current emit is, I think any proposal to change it would ideally acknowledge why the current version is the way it is (or else we presume that it was made weird for no reason) and then argue that the proposed changes would serve those purposes equally well or betterβ¦ or why those purposes donβt need to be served anymore. |
If what you ultimately want is for the enum values to be inlined, why not just skip a step and let TS do that for you via |
@jcalz You'd need to hoist the "get or initialize with empty object" logic (to a I've updated the initial comment to clear this up a bit. I tried accounting for declaration merging (and did address it within single files), but I missed the case of global enums and (more importantly) namespaces defined across multiple separately-compiled scripts, so thanks for the callout. And yes, I'm acutely aware of why it is the way it is right now. Just trying to find ways to make it easier for optimizers (both in engines and in CLI tools) to make sense of it without compromising functionality. |
@fatcerberus Unfortunately, And this choice isn't always made by the individual developer directly - it's sometimes made by a framework (like Vite) on their behalf. |
Both enums and namespaces create their own scope which is broken in the proposed emit. For example, in your version the enum Foo {
ONE,
TWO = ONE,
} ...Unless you emit every enum name as a |
@Josh-Cena Have you actually looked at the current emit of that? If you look closely, the existing emit doesn't even reference |
If the namespace assignment is moved outside of the iife invocation, it becomes just a regular iife. This is already simpler to optimize for minifiers. For example. the following code: namespace Top {
export function fn1(): void {}
}
namespace Top {
export namespace Nested {
export function fn2(): void {}
}
}
namespace Top.Nested {
export function fn3(): void {}
}
enum Top {
a,
b
} Compiles to: "use strict";
var Top;
(function (Top) {
function fn1() { }
Top.fn1 = fn1;
})(Top || (Top = {}));
(function (Top) {
let Nested;
(function (Nested) {
function fn2() { }
Nested.fn2 = fn2;
})(Nested = Top.Nested || (Top.Nested = {}));
})(Top || (Top = {}));
(function (Top) {
var Nested;
(function (Nested) {
function fn3() { }
Nested.fn3 = fn3;
})(Nested = Top.Nested || (Top.Nested = {}));
})(Top || (Top = {}));
var Enum;
(function (Enum) {
Enum[Enum["a"] = 0] = "a";
Enum[Enum["b"] = 1] = "b";
})(Enum || (Enum = {})); which after minification and some formatting becomes let n
let t
;(function (n) {
n.fn1 = function () {}
})(n || (n = {}))
;(function (n) {
let t
!(function (n) {
n.fn2 = function () {}
})((t = n.Nested || (n.Nested = {})))
})(n || (n = {}))
;(function (n) {
;(n.Nested || (n.Nested = {})).fn3 = function () {}
})(n || (n = {}))
;(function (n) {
n[(n.a = 0)] = 'a'
n[(n.b = 1)] = 'b'
})(t || (t = {})) Instead, it could be compiled to this: "use strict";
var Top = Top || {};
Top.Nested ||= {};
(function () {
function fn1() { }
Top.fn1 = fn1;
})();
(function () {
var Nested = Top.Nested;
(function () {
function fn2() { }
Nested.fn2 = fn2;
})();
})();
(function () {
var Nested = Top.Nested;
(function () {
function fn3() { }
Nested.fn3 = fn3;
})();
})();
var Enum = Enum || {};
(function () {
Enum[Enum["a"] = 0] = "a";
Enum[Enum["b"] = 1] = "b";
})(); which after minification and some manual formatting becomes: var Top = Top || {}
Top.Nested ||= {}
Top.fn1 = function () {}
Top.Nested.fn2 = function () {}
Top.Nested.fn3 = function () {}
var Enum = Enum || {}
Enum[0] = 'a'
Enum.a = 0
Enum[1] = 'b'
Enum.b = 1 I understand that TypeScript has to handle module and enum augmentations. But TypeScript also has the concept of module mode vs script mode. In module mode, it could do further optimizations, such as turning the enum into an object literal. |
Suggestion
π Search Terms
enum emit
β Viability Checklist
My suggestion meets these guidelines:
β Suggestion
Suppose we have this code:
Currently, this results in the following emit:
A much better emit would be this (comments explaining each bit):
This would carry the same observable semantics it currently does (complete with
Object.prototype
observability).You may be able to go one step further and just build the object literal directly. This of course is observable (
Object.prototype
setters would not be invoked), but would result in ideal code for most cases. You'd only be able to do this for cases like modules, though.Do want to note that while this can technically be larger for a few enums, it'll only be that for a few, and it'll almost certainly be a wash after compression as well.
π Motivating Example
This reduces namespace and enum code gen overhead significantly and also make it much easier to optimize for both engines (on startup) and optimizer tools.
Currently, Terser has two issues around TypeScript enum generation, and this would serve to fix both.
The first issue linked, terser/terser#1064, features this code:
The current emit when targeting modules is this:
Terser, with
--module --mangle --compress passes=2
, minifies it to this (whitespace added for clarity):My proposed emit would be this:
Terser with the same settings compresses it to just this:
Adding one more pass (
--compress passes=3
) allows it to complete the enum inlining:π» Use Cases
The current approach just quite frankly is extremely difficult to optimize for. Not only is it bloated, but it's also difficult to detect for minification purposes - the motivating example elaborates on this further.
It'd also boost startup speed, even absent the minifier optimizations, since it's only setting properties, not also going through the ceremony of IIFEs plus
Enum || (Enum = {})
.Also, consider this code:
The code it generates is this:
Terser, with
--module --mangle --compress passes=2
, minifies it to this (whitespace added for clarity):My proposed emit would be this:
Once terser/terser#1389 gets resolved (it's about the
export
specifically), Terser should minify the proposed one to this:With
esbuild --minify
, it's not as sophisticated, but benefits are still apparent. Here are the current and new emits side-by-side with it, to show the difference:The text was updated successfully, but these errors were encountered: