Skip to content

Remove two redundant shapes from the JavaScript output - #14

Merged
marevol merged 1 commit into
mainfrom
perf/js-output-size
Sep 5, 2026
Merged

Remove two redundant shapes from the JavaScript output#14
marevol merged 1 commit into
mainfrom
perf/js-output-size

Conversation

@marevol

@marevol marevol commented Sep 5, 2026

Copy link
Copy Markdown
Contributor

Problem

MungedCodeGenerator emits more bytes than it needs to in two places. Both are already recorded in JsGoldenFileTest's jQuery gap table as Release 2 work — this is that work.

1. A ; immediately before a }

function f(){var a=1;return a;}  ->  function f(){var b=1;return b;}   // before
                                 ->  function f(){var b=1;return b}    // after
if(a)b();else c();               ->  if(a){b();}else{c();}             // before
                                 ->  if(a){b()}else{c()}               // after

} ends the statement on its own. Upstream YUI removes it too ("Remove ';' when followed by a '}'", CHANGELOG 1.1), and every golden file in src/test/resources carries zero ;}.

2. Parentheses around a conditional on an assignment's right-hand side

x=y?z:w;  ->  x=(y?z:w);   // before
          ->  x=y?z:w;     // after

An assignment's right-hand side is an AssignmentExpression, which a conditional already is. Upstream emits this without parentheses.

Fix

Every site that emits a block now goes through one visitStatementList, which skips the separator after the last statement. The synthetic braces that if/else and the loop bodies wrap a single statement in never needed a ; at all — it was appended immediately before the } in the same expression.

Switch cases are the one place the position matters: only the last case's last statement is followed by the switch's }. Every other one keeps its ;, otherwise case 1:b()case 2: is a syntax error. Pinned in RedundantOutputTest.

needsParentheses returned true for every ConditionalExpression under any InfixExpression; it now excludes an Assignment's right operand only. Everything else keeps its parentheses, because every other operator binds tighter than ?:x=a+(b?c:d) without them re-parses as (a+b)?c:d.

Line breaks are unaffected. A statement that gave up its ; is still a statement boundary, but nothing in the output says so, so markSafeBreak is no longer offered at that position — the } that follows immediately offers one anyway. Every recorded break offset stays verifiable as following a ; or a }, which is what ModernJsTest.assertEveryLineBreakIsAtAStatementBoundary checks.

Measurements

jquery-1.6.4.js: 104,815 → 103,468 bytes (−1.3%).

contributor to the golden gap before after
missing ; before } +1,259 0
extra parentheses +128 +84
longer munged names +1,280 +1,325
extra spaces +81 +81
fewer braces −62 −62
total gap 2,823 1,476

;} now matches the golden exactly (0). What is left is almost entirely short-name allocation, which stays Release 2 work.

Corpus: 576 real-world scripts (jQuery, Bootstrap, Vue/webpack and Next.js bundles, FullCalendar, DOMPurify, marked, and the Fess/CodeLibs front ends) — 11,436,756 → 11,357,307 bytes, −0.69%. No file grew.

Safety: every one of those 576 outputs was run through node --check. The 39 that do not parse fail identically before this change — none is new, and they are a separate pre-existing issue. No new parse failure and no new second-pass instability across the same corpus.

Test changes

  • Expectations that pinned the old spelling are updated in place. The replacement was driven from the measured (expected, actual) pairs reported by the failing run rather than by hand, so only exact prior expectations were rewritten.
  • RedundantOutputTest (new, 11 cases) pins both new shapes together with the cases that must keep their separator or their parentheses.
  • Size pins in JsGoldenFileTest and IdempotencyTest, and the gap table, are updated with re-measured numbers.
  • issue71.js.min is regenerated. That golden was written for this fork rather than taken from upstream — upstream 2.4.8 drops the "use strict" the fixture exists to test.

Full suite: 775 tests, 0 failures, 3 skipped.

MungedCodeGenerator emitted more bytes than it needed to in two places. Both
are recorded in JsGoldenFileTest's jQuery gap table as Release 2 work; this is
that work.

A ";" immediately before a "}"

  function f(){var a=1;return a;}  ->  function f(){var b=1;return b}
  if(a)b();else c();               ->  if(a){b()}else{c()}

"}" ends the statement on its own, and automatic semicolon insertion covers a
reader that wants one. Upstream YUI removes it too ("Remove ';' when followed
by a '}'", CHANGELOG 1.1) and the golden files carry no ";}" at all.

Every site that emits a block now goes through visitStatementList, which skips
the separator after the last statement. The synthetic braces that if/else and
the loop bodies wrap a single statement in never needed one, since the ";" was
emitted immediately before the "}" in the same expression. Switch cases are the
one place the position matters: only the last case's last statement is followed
by the switch's "}", so every other one keeps its ";" to separate it from the
next "case".

Line breaks are unaffected. A statement that gave up its ";" is still a
statement boundary, but nothing in the output says so, so markSafeBreak is no
longer offered there - the "}" that follows immediately offers one anyway, and
every recorded break offset stays verifiable as following a ";" or a "}".

Parentheses around a conditional on an assignment's right-hand side

  x=y?z:w;  ->  x=y?z:w;   (was "x=(y?z:w);")

An assignment's right-hand side is an AssignmentExpression, which a conditional
already is. needsParentheses returned true for every ConditionalExpression
under any InfixExpression; it now excludes an Assignment's right operand only.
Everything else keeps its parentheses, because every other operator binds
tighter than "?:" - "x=a+(b?c:d)" without them would re-parse as "(a+b)?c:d".

Measurements

- jquery-1.6.4.js: 104,815 -> 103,468 bytes (-1.3%). The gap against the golden
  narrows from 2,823 bytes to 1,476, and ";}" drops from 1,259 to 0, matching
  the golden exactly. The gap table and both size pins are updated; what is
  left of the gap is almost entirely short-name allocation.
- 576 real-world scripts (jQuery, Bootstrap, Vue/webpack and Next.js bundles,
  FullCalendar, DOMPurify, marked and the Fess/CodeLibs front ends):
  11,436,756 -> 11,357,307 bytes, -0.69%. No file grew.
- Every one of those outputs was checked with "node --check". The 39 that do
  not parse fail identically before this change; none is new.
- No new parse failure and no new second-pass instability across the same
  corpus.

Test expectations that pinned the old spelling are updated in place - the
replacement was driven from the measured (expected, actual) pairs rather than
by hand - and RedundantOutputTest pins both new shapes together with the cases
that must keep their separator or their parentheses. issue71.js.min is
regenerated; it was written for this fork rather than taken from upstream,
which drops the "use strict" the fixture exists to test.
@marevol marevol added this to the 2.4.11 milestone Sep 5, 2026
@marevol marevol self-assigned this Sep 5, 2026
@marevol
marevol merged commit e7d4377 into main Sep 5, 2026
6 checks passed
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.

1 participant