Skip to content

Use const in JS library code and enable Terser reduce_vars/unused - #27871

Open
sbc100 wants to merge 1 commit into
emscripten-core:mainfrom
sbc100:js_consts_inline
Open

sbc100 wants to merge 1 commit into
emscripten-core:mainfrom
sbc100:js_consts_inline

Conversation

@sbc100

@sbc100 sbc100 commented Oct 5, 2026 •

Copy link
Copy Markdown
Collaborator

Enable reduce_vars and unused in Terser --compress (added in
#27926) in tools/acorn-optimizer.mjs.

This inlines const values into their expression uses and drops the
resulting unused local const declarations (along with unused function
parameters and single-use inner functions) in optimized builds that do
not run Closure Compiler.

This allows us to use const in JS library code without paying a code
size cost, while reducing total JS code size in
test_no_closure_code_size by 612 bytes.

Timing highlights (test/hello_world.c with -sINCLUDE_FULL_LIBRARY,
~247 KB minified JS):

  • minify_sync with compress: {defaults: false, evaluate: true, keep_fargs: false}: 165.1 ms (250,514 B)
  • minify_sync adding reduce_vars: true, unused: true: 224.3 ms
    (+59.2 ms, or +26.7 ms for unused and +28.8 ms for reduce_vars),
    saving ~3.2 KB (247,346 B).
  • On a standard -O2 hello_world.c build (~8.4 KB JS), minify_sync
    goes from 3.9 ms to 6.2 ms (+2.3 ms), saving 96 B.

@sbc100
sbc100 force-pushed the js_consts_inline branch 3 times, most recently from aadf21e to 6bf76cc Compare October 5, 2026 21:24
@sbc100 sbc100 changed the title [acorn-opt] Add inlineConstants pass to inline primitive JS constants Add inlineConstants pass to inline primitive JS constants Oct 5, 2026
@sbc100 sbc100 changed the title Add inlineConstants pass to inline primitive JS constants Add inlineConstants acorn pass to inline primitive JS constants Oct 5, 2026
@sbc100
sbc100 requested a review from kripken October 5, 2026 22:14
@sbc100

sbc100 commented Oct 5, 2026

Copy link
Copy Markdown
Collaborator Author

Let me know if you would like me to land the pass first, before making the widespread usage withing the library files?

@sbc100
sbc100 force-pushed the js_consts_inline branch 2 times, most recently from c652d23 to 46993d7 Compare October 5, 2026 22:54
@sbc100
sbc100 requested a review from brendandahl October 5, 2026 23:22
@kripken

kripken commented Oct 6, 2026

Copy link
Copy Markdown
Member

What is the codesize cost you are concerned about here - the const keyword itself, or something else?

@sbc100

sbc100 commented Oct 6, 2026

Copy link
Copy Markdown
Collaborator Author

What is the codesize cost you are concerned about here - the const keyword itself, or something else?

There are two consts:

  1. The const keyword is 2 bytes longer.
  2. let and const declarations of block-scoped and so cannot be combined into a single function level declaration (unlike var). With var know that closure can combine all them into a jut one single var per function.

With this inlining pass we don't pay either of these costs!

@kripken

kripken commented Oct 6, 2026

Copy link
Copy Markdown
Member

I see, thanks.

Can we get both of those benefits by just renaming const to var? That seems simpler but maybe I'm missing something?

@sbc100

sbc100 commented Oct 6, 2026

Copy link
Copy Markdown
Collaborator Author

I see, thanks.

Can we get both of those benefits by just renaming const to var? That seems simpler but maybe I'm missing something?

Maybe, that seems like a rather odd way to achieve the goal though. What is it about the inlining approach the you don't like?

@kripken

kripken commented Oct 6, 2026

Copy link
Copy Markdown
Member

Just the complexity. It has to consider scoping, and it is around 100 lines. I think const=>var would be almost a one-liner?

@sbc100

sbc100 commented Oct 6, 2026

Copy link
Copy Markdown
Collaborator Author

One other motivation I should mention is that this is needed for another change I have in the pipeline: cd9fbb5

@kripken

kripken commented Oct 6, 2026

Copy link
Copy Markdown
Member

More generally, these passes are not simple to read or write, so fewer and simpler ones seems worthwhile to me.

@sbc100

sbc100 commented Oct 6, 2026

Copy link
Copy Markdown
Collaborator Author

The other reason we cannot just do const -> var here is that we want users of -Oz and -Os to have not pay the cost of these variables at all. I.e. I want to be create to create new const XX = YY variables without having to worry about the codesize costs for non-closure users.

An alternative is that we could run a full terser minifier in these most? I think terser will do constant inlining by default.

@kripken

kripken commented Oct 6, 2026 •

Copy link
Copy Markdown
Member

I want to be create to create new const XX = YY variables without having to worry about the codesize costs for non-closure users.

Doesn't const=>var fix that? Or do you mean we'd need to also fuse together the original var and the former-const-but-now-var? edit: oh, I see, you mean adding more for code clarity, so we'd really need to fuse them all to save size

An alternative is that we could run a full terser minifier in these most? I think terser will do constant inlining by default.

Yeah, that sort of makes sense to me. I'd say that Acorn passes are necessary when we are doing something that depends on Emscripten semantics (like safe-heap rewrite or export parsing/removal in metadce). But it feels a little wrong to start to replicate standard compiler optimizations in Terser and Closure?

I'm not totally opposed to trivial ones, though. Especially if Terser is slow (but I didn't check if it is)

@sbc100

sbc100 commented Oct 6, 2026

Copy link
Copy Markdown
Collaborator Author

I want to be create to create new const XX = YY variables without having to worry about the codesize costs for non-closure users.

Doesn't const=>var fix that? Or do you mean we'd need to also fuse together the original var and the former-const-but-now-var? edit: oh, I see, you mean adding more for code clarity, so we'd really need to fuse them all to save size

But I think we really want to fully inline these, not just fuse them.. so the cost of adding a new const XXX = YY; (rather then having YY just inline) drops to zero, even for non-closure users.

An alternative is that we could run a full terser minifier in these most? I think terser will do constant inlining by default.

Yeah, that sort of makes sense to me. I'd say that Acorn passes are necessary when we are doing something that depends on Emscripten semantics (like safe-heap rewrite or export parsing/removal in metadce). But it feels a little wrong to start to replicate standard compiler optimizations in Terser and Closure?

I'm not totally opposed to trivial ones, though. Especially if Terser is slow (but I didn't check if it is)

Ok, maybe I'll look into integrating terser for -O3 / -Oz and -Os users then? That would avoid the perenial question of "does the regress non-closure users".

sbc100 added a commit that referenced this pull request Oct 7, 2026
This allows is to track the size of our builds for users who are not
using closure compiler.Add a codesize test that does not use closure
compiler

Split out from #27871
@kripken

kripken commented Oct 7, 2026

Copy link
Copy Markdown
Member

I agree that running Terser makes sense. Seems better than working on our own JS opts. My only concern is if it makes compile times a lot slower (but hopefully not, or hopefully there is a flag to control how much it does?)

@sbc100

sbc100 commented Oct 7, 2026

Copy link
Copy Markdown
Collaborator Author

I agree that running Terser makes sense. Seems better than working on our own JS opts. My only concern is if it makes compile times a lot slower (but hopefully not, or hopefully there is a flag to control how much it does?)

I imagine it would only be fore -O3/-Oz/-Os/etc users so they already have a slow link time IIUC. And yes I imagine you could disable it completely with --minify=0.

sbc100 added a commit to sbc100/emscripten that referenced this pull request Oct 8, 2026
Instead of a dedicated `stripDefaultUndefined` Acorn AST pass (added in
compression (`{defaults: false, evaluate: true, keep_fargs: false}`)
when minifying JS without Closure Compiler. In addition to stripping
`= undefined` default parameter values, this performs lightweight Terser
cleanups (`undefined` -> `void 0`, single-statement block brace
removal, and constant folding).

In general, this prevents us from needing to replicate compression
passes that already exist in upstream Terser (see emscripten-core#27871).

Timing highlights (`test/hello_world.c` with `-sINCLUDE_FULL_LIBRARY`,
~252 KB minified JS):
- `minify_sync` with `compress: false`: 105.5 ms
- `minify_sync` with `compress: {defaults: false, evaluate: true,
  keep_fargs: false}`: 188.3 ms (+82.8 ms, or ~+63 ms net after
  removing the `stripDefaultUndefined` AST walk), saving ~1.4 KB.
- On a standard `-O2` `hello_world.c` build (~8.5 KB JS), `minify_sync`
  goes from 3.2 ms to 5.5 ms (+2.3 ms).

This change, in general, should prevent us from being tempted to
replicate compression passes that already exist in upstream terser
(See emscripten-core#27871, for example).

See: emscripten-core#27839
sbc100 added a commit to sbc100/emscripten that referenced this pull request Oct 8, 2026
Instead of a dedicated `stripDefaultUndefined` Acorn AST pass (added in
compression (`{defaults: false, evaluate: true, keep_fargs: false}`)
when minifying JS without Closure Compiler. In addition to stripping
`= undefined` default parameter values, this performs lightweight Terser
cleanups (`undefined` -> `void 0`, single-statement block brace
removal, and constant folding).

In general, this prevents us from needing to replicate compression
passes that already exist in upstream Terser (see emscripten-core#27871).

Timing highlights (`test/hello_world.c` with `-sINCLUDE_FULL_LIBRARY`,
~252 KB minified JS):
- `minify_sync` with `compress: false`: 105.5 ms
- `minify_sync` with `compress: {defaults: false, evaluate: true,
  keep_fargs: false}`: 188.3 ms (+82.8 ms, or ~+63 ms net after
  removing the `stripDefaultUndefined` AST walk), saving ~1.4 KB.
- On a standard `-O2` `hello_world.c` build (~8.5 KB JS), `minify_sync`
  goes from 3.2 ms to 5.5 ms (+2.3 ms).

This change, in general, should prevent us from being tempted to
replicate compression passes that already exist in upstream terser
(See emscripten-core#27871, for example).

See: emscripten-core#27839
sbc100 added a commit to sbc100/emscripten that referenced this pull request Oct 8, 2026
Instead of a dedicated `stripDefaultUndefined` Acorn AST pass (added in
compression (`{defaults: false, evaluate: true, keep_fargs: false}`)
when minifying JS without Closure Compiler. In addition to stripping
`= undefined` default parameter values, this performs lightweight Terser
cleanups (`undefined` -> `void 0`, single-statement block brace
removal, and constant folding).

In general, this prevents us from needing to replicate compression
passes that already exist in upstream Terser (see emscripten-core#27871).

Timing highlights (`test/hello_world.c` with `-sINCLUDE_FULL_LIBRARY`,
~252 KB minified JS):
- `minify_sync` with `compress: false`: 105.5 ms
- `minify_sync` with `compress: {defaults: false, evaluate: true,
  keep_fargs: false}`: 188.3 ms (+82.8 ms, or ~+63 ms net after
  removing the `stripDefaultUndefined` AST walk), saving ~1.4 KB.
- On a standard `-O2` `hello_world.c` build (~8.5 KB JS), `minify_sync`
  goes from 3.2 ms to 5.5 ms (+2.3 ms).

This change, in general, should prevent us from being tempted to
replicate compression passes that already exist in upstream terser
(See emscripten-core#27871, for example).

See: emscripten-core#27839
sbc100 added a commit to sbc100/emscripten that referenced this pull request Oct 8, 2026
Instead of a dedicated `stripDefaultUndefined` Acorn AST pass (added in
compression (`{defaults: false, evaluate: true, keep_fargs: false}`)
when minifying JS without Closure Compiler. In addition to stripping
`= undefined` default parameter values, this performs lightweight Terser
cleanups (`undefined` -> `void 0`, single-statement block brace
removal, and constant folding).

In general, this prevents us from needing to replicate compression
passes that already exist in upstream Terser (see emscripten-core#27871).

Timing highlights (`test/hello_world.c` with `-sINCLUDE_FULL_LIBRARY`,
~252 KB minified JS):
- `minify_sync` with `compress: false`: 105.5 ms
- `minify_sync` with `compress: {defaults: false, evaluate: true,
  keep_fargs: false}`: 188.3 ms (+82.8 ms, or ~+63 ms net after
  removing the `stripDefaultUndefined` AST walk), saving ~1.4 KB.
- On a standard `-O2` `hello_world.c` build (~8.5 KB JS), `minify_sync`
  goes from 3.2 ms to 5.5 ms (+2.3 ms).

This change, in general, should prevent us from being tempted to
replicate compression passes that already exist in upstream terser
(See emscripten-core#27871, for example).

See: emscripten-core#27839
sbc100 added a commit that referenced this pull request Oct 9, 2026
Instead of a dedicated `stripDefaultUndefined` Acorn AST pass (added in
compression (`{defaults: false, evaluate: true, keep_fargs: false}`)
when minifying JS without Closure Compiler. In addition to stripping
`= undefined` default parameter values, this performs lightweight Terser
cleanups (`undefined` -> `void 0`, single-statement block brace
removal, and constant folding).

In general, this prevents us from needing to replicate compression
passes that already exist in upstream Terser (see #27871).

Timing highlights (`test/hello_world.c` with `-sINCLUDE_FULL_LIBRARY`,
~252 KB minified JS):
- `minify_sync` with `compress: false`: 105.5 ms
- `minify_sync` with `compress: {defaults: false, evaluate: true,
  keep_fargs: false}`: 188.3 ms (+82.8 ms, or ~+63 ms net after
  removing the `stripDefaultUndefined` AST walk), saving ~1.4 KB.
- On a standard `-O2` `hello_world.c` build (~8.5 KB JS), `minify_sync`
  goes from 3.2 ms to 5.5 ms (+2.3 ms).

This change, in general, should prevent us from being tempted to
replicate compression passes that already exist in upstream terser
(See #27871, for example).

See: #27839
@sbc100 sbc100 changed the title Add inlineConstants acorn pass to inline primitive JS constants Use const in JS library code Oct 9, 2026
@sbc100

sbc100 commented Oct 9, 2026

Copy link
Copy Markdown
Collaborator Author

OK, this change now just starts using JS const in more places, now that terser is run on -O2 and above.

@sbc100 sbc100 changed the title Use const in JS library code Use const in JS library code and enable Terser reduce_vars/unused Oct 9, 2026
Enable `reduce_vars` and `unused` in Terser `--compress` (added in
emscripten-core#27926) in `tools/acorn-optimizer.mjs`.

This inlines `const` values into their expression uses and drops the
resulting unused local `const` declarations (along with unused function
parameters and single-use inner functions) in optimized builds that do
not run Closure Compiler.

This allows us to use `const` in JS library code without paying a code
size cost, while reducing total JS code size in
`test_no_closure_code_size` by 612 bytes.

Timing highlights (`test/hello_world.c` with `-sINCLUDE_FULL_LIBRARY`,
~247 KB minified JS):
- `minify_sync` with `compress: {defaults: false, evaluate: true,
  keep_fargs: false}`: 165.1 ms (250,514 B)
- `minify_sync` adding `reduce_vars: true, unused: true`: 224.3 ms
  (+59.2 ms, or +26.7 ms for `unused` and +28.8 ms for `reduce_vars`),
  saving ~3.2 KB (247,346 B).
- On a standard `-O2` `hello_world.c` build (~8.4 KB JS), `minify_sync`
  goes from 3.9 ms to 6.2 ms (+2.3 ms), saving 96 B.
@sbc100

sbc100 commented Oct 9, 2026

Copy link
Copy Markdown
Collaborator Author

I think I might split out the terser config changes and land them first..

This branch has not been deployed

No deployments
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

Successfully merging this pull request may close these issues.

2 participants