Repository navigation
feat(minify): close more oxc and swc gaps, keep with bodies free of flattened names - #22919
Conversation
… strict arrow calling eval in place Claude-Session: https://claude.ai/code/session_01ViD41i2KdwwGxrDk7yrjeD
…s into for heads, keep exported names cheaper unmangled Claude-Session: https://claude.ai/code/session_01ViD41i2KdwwGxrDk7yrjeD
…teral IIFE arguments and negated literals Claude-Session: https://claude.ai/code/session_01ViD41i2KdwwGxrDk7yrjeD
terser flattened such a call into the body, so its `var`s and parameters became names the `with` object could hold. Claude-Session: https://claude.ai/code/session_01ViD41i2KdwwGxrDk7yrjeD
As long as the substitution it replaced, it gzipped 17 bytes worse on test262's hebrew-month-codes. Claude-Session: https://claude.ai/code/session_01ViD41i2KdwwGxrDk7yrjeD
🦋 Changeset detectedLatest commit: 2da6c6b The changes in this PR will be included in the next version bump. This PR includes changesets to release 1 package
Not sure what this means? Click here to learn what changesets are. Click here if you're a maintainer who wants to add another changeset to this PR |
|
This PR is packaged and the instant preview of its latest commit is available. Install it locally:
npm i -D webpack@https://pkg.pr.new/webpack@22919
yarn add -D webpack@https://pkg.pr.new/webpack@22919
pnpm add -D webpack@https://pkg.pr.new/webpack@22919 |
Types CoverageCoverage after merging feat/minify-more-gaps into main will be
Coverage Report |
Generated code sizeComparing base (
Read 3 asset(s) changed size, test untouched
No runtime gained or lost a runtime module. Built |
|
No actionable comments were generated in the recent review.
|
| Layer / File(s) | Summary |
|---|---|
Expression evaluation and folding lib/javascript/syntax-printer.js, test/unitCases/javascript/printer.unittest.js, test/helpers/oxcSmaller.js, test/helpers/swcSmaller.js, test/specCases/minify-corpora.spectest.js, docs/syntax.md, .changeset/067-minify-more-gaps.md |
Primitive classification and expression optimization cover additional constant, template, typeof, and logical-expression cases. Tests and comparison records cover the changed cases. The documentation and Changeset describe minification behavior and release changes. |
Name handling in with scopes lib/javascript/ScopeAnalyzer.js, lib/javascript/syntax-printer.js, test/unitCases/javascript/printer.unittest.js, test/helpers/oxcSmaller.js, test/helpers/swcSmaller.js |
Both scope analyzers can collect names referenced in with bodies. Mangling uses the collected names, and inlining avoids moving certain functions into with bodies. Tests cover name resolution and mangling behavior. |
Call inlining and asm.js statements lib/javascript/syntax-printer.js, test/unitCases/javascript/printer.unittest.js, test/helpers/oxcSmaller.js, test/helpers/swcSmaller.js |
Eligible calls support literal-argument substitution and strict-mode direct-eval handling. The compressor removes eligible constant and function expression statements in asm.js scopes. |
Loop declaration placement lib/javascript/syntax-printer.js, test/unitCases/javascript/printer.unittest.js, test/helpers/oxcSmaller.js, docs/syntax.md |
Declaration movement covers eligible for…in/for…of loops and following for initializers. Tests cover cases where declarations remain outside loop heads. |
Possibly related PRs
- webpack/webpack#22897: Both changes address name handling in
withscopes. - webpack/webpack#22909: Both changes add function and value inlining in
lib/javascript/syntax-printer.js. - webpack/webpack#22873: Both changes modify loop declaration placement and template substitution.
Suggested labels: area: parser
Suggested reviewers: aryanraj45
Priority:
Change: Feature
Merge Risk: 
Minimal · up to 2da6c
-
The minification changes have no established merge-blocking issue and are mergeable subject to normal checks.
Pre-merge checks: 
3 · 
1
-
Failed checks: 1 inconclusive Passed checks: 3 passed -
- Autofix · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts
Warning
Some tools did not complete. Review the errors below.
🔧 ast-grep (0.45.3)
-
test/unitCases/javascript/printer.unittest.js
-
ast-grep timed out on this file
lib/javascript/syntax-printer.js
-
ast-grep timed out on this file
-
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.
Comment @coderabbitai help to get the list of available commands.
Merging this PR will improve performance by 33.44%2 warnings, 1 note
Performance Changes
Tip Curious why performance improved? Comment Comparing |
…und (#22903) * fix(html): cover the printer from its own tests and fix what that found Prints that re-parsed to a different tree, now each printed so the tree comes back the same: - content after a `<plaintext>` no longer gets end tags that read as text; a script left double-escaped at EOF gets no `</script>`; - a `<?pi>` keeps the shell tags around it, as a comment does; - `<body>` stays before a head element, a `<noscript>`, a template or whitespace collapsing removes; `<html>` in foreign content keeps its start tag; `<tbody>`/`<colgroup>` in template content and `</caption>` before a script are kept; - nested forms, text fostered out of a table and merged into earlier text, and fragment prints (whitespace under the root, a stray `<html>`, non-body contexts) are re-parsed and fall back to the source. A streamed walk starting after a `</form>` no longer visits out of order, `skipChildren()` while printing no longer writes `undefined`, and `<tr> <table><tr><option><li>` no longer throws. The printer tests now reach every statement and branch of the HTML printer and minifier; checks no input could fail are folded away. Claude-Session: https://claude.ai/code/session_017TkBWTk3wcyUcJYDp5ZhaP * fix(html): cover the insertion modes from the parser tests too The parser tests' earlier reach left out the insertion modes (`modes.*`), which are assigned after `const modes = {}`; they now reach every statement and branch of those too, and two trees were wrong: - an `<a>` start tag ran the adoption agency, then removed the first `a` anywhere in the active formatting list, which can be one below a marker: in `<a>1<marquee><a>2<a>3</marquee>4` the outer link was lost. - in a head `<noscript>` with scripting off, the whitespace before other text went to the head after it; the mode now splits it off first, as in head and after head do. Checks the stack and list invariants rule out (an `<html>` at the bottom of every scope, a formatting element still listed after the inner loop, a detached node always among its parent's children, …) are folded away. Claude-Session: https://claude.ai/code/session_017TkBWTk3wcyUcJYDp5ZhaP * fix(css): cover the printer from its own tests and fix what that found Prints that changed what a sheet means, or threw: - `background-position` with an empty layer printed `undefined` and turned a value the browser drops into one it applies; - a `url()` holding two strings was rebuilt as one valid url; - `@supports ((display:grid) {…}` cut short lost its outer `(`, so the block opened and the rule applied; - box longhands holding a word the longhand does not read were merged into a shorthand the browser drops whole (and the shorthand lowered into partly valid longhands); - `escapeIdentifier` wrote U+0001–U+001F/U+007F raw and a lone `-` bare; `normalizeUrl` decoded `\1F600` as U+F600 and `\110000` as U+0000; - nesting lowered inside a block big enough to stream lost the hoisted rules (an empty sheet for a 4000-rule block); - `skipChildren()` while printing, `color-mix()` with an empty argument and `as: "block-contents"` with a nested or streamed block threw. The printer tests now reach every statement and branch of syntax-printer.js and every printer function in syntax-parser.js; checks no input could fail are folded away. Claude-Session: https://claude.ai/code/session_017TkBWTk3wcyUcJYDp5ZhaP * fix(css): cover the rest of the printer and fix what that found Ordered and deduplicated layer gathers, empty @layer blocks no longer stop later joins, value-less grid longhands no longer merge, and an unquoted url no longer reads as a comment when scanning layer text. Claude-Session: https://claude.ai/code/session_017TkBWTk3wcyUcJYDp5ZhaP * fix(javascript): cover the printer from its own tests and fix what that found Nested IIFE run-in-place crash, uncompilable RegExp() under unsafe, folding patched builtins, global_defs on an exported binding, RegExp.$1 under unsafe, ESTree shorthand __proto__, and spidermonkey directive values. Claude-Session: https://claude.ai/code/session_017TkBWTk3wcyUcJYDp5ZhaP * fix(javascript): reach more of the printer and fix what that found A join of one hole, hoist_vars across a block with let, a computed __proto__ key under unsafe, and new RegExp(/a/g) losing its flags. Claude-Session: https://claude.ai/code/session_017TkBWTk3wcyUcJYDp5ZhaP * chore: add the printer changeset and fix the type check Claude-Session: https://claude.ai/code/session_017TkBWTk3wcyUcJYDp5ZhaP * fix(css): lower an inset of a lone CSS-wide keyword again The box-slot check refused `inset:inherit`, which every longhand reads; a keyword beside another value still stays, as the target drops it. Claude-Session: https://claude.ai/code/session_017TkBWTk3wcyUcJYDp5ZhaP * fix(javascript): keep the literal a built-in method is called on A computed read like `[1]['concat']` rewritten to a dot read lost its receiver's elements, as the call guard compared the rewritten read with the one its parent calls. Claude-Session: https://claude.ai/code/session_017TkBWTk3wcyUcJYDp5ZhaP * test(javascript): read a duplicate-named regular expression by the runtime Duplicate group names in alternatives compile on newer runtimes only, so whether its `source` folds follows the runtime the test runs on. Claude-Session: https://claude.ai/code/session_017TkBWTk3wcyUcJYDp5ZhaP * fix(css): tell a quoted url from an unquoted one past leading whitespace The lookahead backtracked into the whitespace, so `url( "a)b")` read as unquoted. Claude-Session: https://claude.ai/code/session_017TkBWTk3wcyUcJYDp5ZhaP * fix(javascript): read the global object as handed on only where it may be, and run the printer tests on Bun A binding nothing reads, a literal's method and a module's function leak no global object, which kept swc's corpus bigger. The printer tests compare runtime output only where the engine compiles the source, read engine errors by type, and skip what a small stack stops. Claude-Session: https://claude.ai/code/session_017TkBWTk3wcyUcJYDp5ZhaP * perf(html): read back only the fragments a body does not read alike Every fragment's print was parsed again to check it, which doubled the cost of one in a `tr`; foreign, `select` and raw-text contexts still are. Claude-Session: https://claude.ai/code/session_017TkBWTk3wcyUcJYDp5ZhaP * test(javascript): leave acorn's deepest program out where Bun's stack is small Bun runs a test in a worker whose stack no parser gets 2000 levels deep in, which every corpus test now reads from one list. Claude-Session: https://claude.ai/code/session_017TkBWTk3wcyUcJYDp5ZhaP * fix(javascript): keep a nested index map's section that ends past the outer one A section's stop clamped the outer stop's column to the next section's even where that one started on a later line, which cut a nested map's first source to nothing; terser's source-map reader does the same. Claude-Session: https://claude.ai/code/session_017TkBWTk3wcyUcJYDp5ZhaP * test(css): read a visitor's replace and remove through the printer's own tests #22906's edits are written out by the printer, which its own tests now read, a replaced node holding a replaced one included. Claude-Session: https://claude.ai/code/session_017TkBWTk3wcyUcJYDp5ZhaP * test(javascript): take the printer's snapshots as main's new passes write them Claude-Session: https://claude.ai/code/session_017TkBWTk3wcyUcJYDp5ZhaP * fix(css): read past the comments after a visitor stops the walk stop() drops the comment bucket while the node it is in still tokenizes, so on a walk that prints nothing the next comment read a bucket it no longer had and threw. The guard folded as dead is back, with its tests. Claude-Session: https://claude.ai/code/session_017TkBWTk3wcyUcJYDp5ZhaP * test(javascript): take the printer's snapshots as #22919's passes write them Claude-Session: https://claude.ai/code/session_017TkBWTk3wcyUcJYDp5ZhaP
Summary
Closes more of the size gaps between webpack's JS minifier and swc/oxc, and fixes a wrong output terser shares.
Fix (
correctphase): a function called inside awithbody was flattened into it, so itsvars and parameters became names thewithobject could hold:New
improverewrites:(a,0)&&b→(a,0),(a,1)&&b→(a,b)(||alike).evalis the call it makes.varmoves into a laterfor…in/for…ofhead, and avarwith a value joins a followingfor(var …)head.typeofof a function or class declaration nothing writes is"function", and a short constant read in a template is written into its text.[…]is!1after its effects.withreaches, which nowithbody spells, are mangled.correctphase: names whose values are numeric expressions are now known to hold numbers, so unread arithmetic on them is dropped.Gap lists:
swcSmaller.js−7,oxcSmaller.js−14, nothing added. TwoIMPROVED_YET_BIGGERentries, each 3 or fewer bytes of gzip.compress: { passes: 2 }, mangle: true)main(raw/gzip)var k; if (o) for (k in o) …var o;if(f)for(o in f)…: 60/77if(f)for(var o in f)…: 58/75What kind of change does this PR introduce?
feat
Did you add tests for your changes?
Yes:
test/unitCases/javascript/printer.unittest.jscovers each rewrite and the cases that must stay unchanged. It also has a regression test for thewithfix. The gap lists andIMPROVED_YET_BIGGERlive intest/specCases/minify-corpora.spectest.js.Does this PR introduce a breaking change?
No.
If relevant, what needs to be documented once your changes are merged or what have you already documented?
docs/syntax.md(thesyntax-printer.jsbullet) describes the new rewrites.Use of AI
Claude Code wrote the changes and tests. Four agents each triaged one family of gap-list entries. The results were merged and then checked with the printer unit tests, the full corpus suite, the minifier config cases, the fuzz seeds and
yarn lint.Generated by Claude Code
Summary by CodeRabbit
withscopes, and expanded safe simplifications fortypeofand asm.js code.