Skip to content

Commit 358df69

Browse files
refactor(javascript): minify through webpack's own entry point (#22217)
* refactor(javascript): minify through webpack's own terser entry point Adds lib/javascript/jsMinify.js beside cssMinify and htmlMinify, so the default minimizer dispatches JavaScript to a minify function webpack owns rather than to the one minimizer-webpack-plugin ships, and promotes terser to a direct dependency. Output is unchanged: a unit test holds jsMinify to the plugin's terserMinify over every comment-selection, production and source-map shape. * perf(javascript): skip the comment callback where nothing is kept or extracted Adds webpack rows to the printing tables of `compare-js-tools`, measuring `jsMinify` itself, so a phase webpack takes over is read against what it replaced. The first thing they showed was our own overhead: handing terser a comments callback when the answer is "keep none" cost ~20% of the print. Output is unchanged. * perf(javascript): mangle names in webpack rather than in terser Adds a phase skeleton: lib/javascript/terser.js loads terser's own sources — its published entry point exposes only `minify`, while a phase needs the classes behind it — and installs the phases webpack implements, each one declaring through `supports` what it reads. A terser that moved any of it, or a runtime without dynamic import, keeps terser's own. The first phase is name mangling. terser rereads a scope's enclosed definitions for every candidate name, which is quadratic in the one big scope a bundle has; asking a set built once is not. Mangling drops 29-47% on large bundles (2958ms to 1572ms on typescript.js), 3.7-4.8% off a whole minify run, with byte-identical output. * refactor(javascript): name the minifier seam after what it becomes `terser.js` and `terserMangle.js` are now `syntax-printer.js` and `syntax-mangle.js`, and the public entry is `webpack.javascript.printer`. terser is what stands behind the seam today, not what the seam is. * refactor(javascript): give JavaScript the syntax pair css and html have `syntax.js` is now the barrel naming both halves — `parser` and `printer` — reached through getters, so parsing never loads the printer and printing never loads the parser. What was `syntax.js` is `syntax-parser.js`, and `webpack.javascript.syntax` is the public face, as for css and html. Two merges while the names moved: the mangle phase into the printer it is installed by, and `ChunkFormatHelpers` into `ChunkHelpers` — the latter reads `StartupHelpers` inside the hash handler, since those two would otherwise wait on each other at load time. * refactor(javascript): keep the parser's Unicode tables in one module `regexpData.js` folded into `data.js`, and the generator emits one file rather than two. It costs every build the 21KB of property tables a rejected pattern used to pay for on its own: measured at 1.18ms and 128KB of heap for the module, against 1.17ms and 81KB before. * refactor(javascript): one parser file instead of two `parser.js` folded into `syntax-parser.js`, which had grown its own copies of 23 of its definitions — the scope and binding constants, `nextLineBreak`, `stringToNumber`, `getLineInfo` and the rest. Two names could not survive the union: the class `Node` collided with the `NodeLike` alias the layer above reads under that name, and `SourceLocation` with estree's, so both classes now say `Parser` in their names. * refactor(javascript): fold the grammar into the parser The grammar lived in its own file and was copied onto `Parser.prototype` on the first parse. A build that parses at all pays that install, so the laziness bought nothing, and patching the prototype after the class exists costs what it saves: parsing typescript.js runs ~15% faster with the productions declared in the class body (warm median 915ms vs 1084ms over 7 interleaved processes per arm; terser's bundle ~8% faster). `regexp.js` stays lazy — only a pattern the host engine itself rejected reaches it. * chore: fold the grammar merge into the parsing changeset * fix(test): point the parser benchmark at syntax-parser `lib/javascript/syntax.js` is the parser/printer barrel now, so the `js-parser-unit` benchmark read `WebpackParser` as undefined and the benchmark-memory job died on it. Also updates the prose still naming the old path. * fix(test): keep jsMinify out of coverage instrumentation `minimizer-webpack-plugin` ships a minify function to its workers as source, so the counters istanbul injects resolve to nothing there and every production build under coverage failed with `cov_… is not defined`. cssMinify and htmlMinify are excluded for that reason already. * chore: keep the changeset sentence within 80 characters * fix(test): state the printer's fallback where terser's sources cannot load Deno cannot import terser's own sources through `load`, so it hands back terser's published entry point with no phase installed — which the test asserted could never happen. It now states both halves of that contract.
1 parent 6b15260 commit 358df69

41 files changed

Lines changed: 20095 additions & 18653 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

‎.changeset/030-javascript-attached-comments.md‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,4 +2,4 @@
22
"webpack": patch
33
---
44

5-
Speed up JavaScript parsing: attached comment lookup and the tokenizer.
5+
Speed up JavaScript parsing: comment lookup, the tokenizer and the grammar.
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"webpack": patch
3+
---
4+
5+
Minify JavaScript through webpack's terser entry point, mangling names faster.

‎AGENTS.md‎

Lines changed: 4 additions & 4 deletions
Large diffs are not rendered by default.

‎cspell.json‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -603,7 +603,7 @@
603603
"**/**/*.snap",
604604
"test/cases/json/weird-properties/globals.json",
605605
"test/unitCases/JavascriptParser.unittest.js",
606-
"lib/javascript/regexpData.js",
606+
"lib/javascript/data.js",
607607
"tooling/html-entities.json",
608608
"test/external/test262-cases/**",
609609
"test/external/wpt/**",

‎eslint.config.mjs‎

Lines changed: 3 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -382,13 +382,9 @@ export default defineConfig([
382382
}
383383
},
384384
{
385-
// The parser keeps acorn's method and property names, since
386-
// `lib/javascript/syntax.js` and any acorn plugin call them by those names.
387-
files: [
388-
"lib/javascript/grammar.js",
389-
"lib/javascript/parser.js",
390-
"lib/javascript/regexp.js"
391-
],
385+
// The parser keeps acorn's method and property names, since any acorn
386+
// plugin calls them by those names.
387+
files: ["lib/javascript/regexp.js", "lib/javascript/syntax-parser.js"],
392388
rules: {
393389
camelcase: "off"
394390
}

‎jest.config.js‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,7 @@ const config = {
4949
"\\.runtime\\.js$",
5050
// Serialized to the minimizer's worker pool as source (like runtime code):
5151
// coverage instrumentation would inject counters the worker can't resolve.
52-
"[\\\\/]lib[\\\\/](?:css[\\\\/]cssMinify|html[\\\\/]htmlMinify)\\.js$",
52+
"[\\\\/]lib[\\\\/](?:css[\\\\/]cssMinify|html[\\\\/]htmlMinify|javascript[\\\\/]jsMinify)\\.js$",
5353
"<rootDir>/test",
5454
"<rootDir>/schemas",
5555
"<rootDir>/examples",

‎lib/FileSystemInfo.js‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ const nodeModule = require("module");
99
const { isAbsolute } = require("path");
1010
const { create: createResolver } = require("enhanced-resolve");
1111
const { DEFAULTS } = require("./config/defaults");
12-
const { collectCjsRequireSpecifiers } = require("./javascript/syntax");
12+
const { collectCjsRequireSpecifiers } = require("./javascript/syntax-parser");
1313
const AsyncQueue = require("./util/AsyncQueue");
1414
const StackedCacheMap = require("./util/StackedCacheMap");
1515
const asyncLib = require("./util/async");

‎lib/asset/WebManifestParser.js‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77

88
const Parser = require("../Parser");
99
const HtmlSourceDependency = require("../dependencies/HtmlSourceDependency");
10-
const { parse } = require("../javascript/syntax");
10+
const { parse } = require("../javascript/syntax-parser");
1111

1212
/** @import { Expression, ObjectExpression, Program, Property } from "estree" */
1313
/** @import { BuildInfo } from "../module/Module" */

‎lib/config/defaults.js‎

Lines changed: 11 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -2524,11 +2524,11 @@ const applyOptimizationDefaults = (
25242524

25252525
// WHY: one minimizer instance, so one worker-thread pool, handles JS and
25262526
// — where native CSS and HTML are enabled — those assets too, as an array
2527-
// of minify functions each dispatched by its own `filter`: terser to JS,
2528-
// `cssMinify` to CSS, `htmlMinify` to HTML. The CSS and HTML serializers
2529-
// stay worker-safe by resolving their parser with `require("webpack/…")`
2530-
// in the worker, so all three share the pool, and `test` is widened to let
2531-
// those assets reach the dispatcher.
2527+
// of minify functions each dispatched by its own `filter`: `jsMinify`
2528+
// (terser) to JS, `cssMinify` to CSS, `htmlMinify` to HTML. The CSS and
2529+
// HTML serializers stay worker-safe by resolving their parser with
2530+
// `require("webpack/…")` in the worker, so all three share the pool, and
2531+
// `test` is widened to let those assets reach the dispatcher.
25322532

25332533
// Step aside for a minimizer the user configured for these types:
25342534
// whichever ran first would mark the asset `minimized` and silently
@@ -2557,12 +2557,13 @@ const applyOptimizationDefaults = (
25572557
minimizeByType.html !== false;
25582558
if (!minifyJs && !minifyCss && !minifyHtml) return;
25592559
if (minifyJs && !minifyCss && !minifyHtml) {
2560-
new TerserPlugin({ terserOptions }).apply(
2561-
/** @type {EXPECTED_ANY} */ (compiler)
2562-
);
2560+
new TerserPlugin({
2561+
minify: require("../javascript/jsMinify"),
2562+
minimizerOptions: terserOptions
2563+
}).apply(/** @type {EXPECTED_ANY} */ (compiler));
25632564
return;
25642565
}
2565-
/** @type {(typeof TerserPlugin.terserMinify | typeof import("../css/cssMinify") | typeof import("../html/htmlMinify"))[]} */
2566+
/** @type {(typeof import("../javascript/jsMinify") | typeof import("../css/cssMinify") | typeof import("../html/htmlMinify"))[]} */
25662567
const minify = [];
25672568
/** @type {EXPECTED_OBJECT[]} */
25682569
const minimizerOptions = [];
@@ -2594,7 +2595,7 @@ const applyOptimizationDefaults = (
25942595
? minimizeByType.html
25952596
: undefined;
25962597
if (minifyJs) {
2597-
minify.push(TerserPlugin.terserMinify);
2598+
minify.push(require("../javascript/jsMinify"));
25982599
minimizerOptions.push(terserOptions);
25992600
extensions.push("[cm]?js");
26002601
}

‎lib/esm/ModuleChunkFormatPlugin.js‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ const { HotUpdateChunk, RuntimeGlobals } = require("..");
1010
const {
1111
createChunkHashHandler,
1212
getChunkInfo
13-
} = require("../javascript/ChunkFormatHelpers");
13+
} = require("../javascript/ChunkHelpers");
1414
const { getAllChunks } = require("../javascript/ChunkHelpers");
1515
const {
1616
chunkHasJs,

0 commit comments

Comments
 (0)