Skip to content

Commit ff27449

Browse files
perf(runtime): emit less generated runtime code (#21803)
* perf(runtime): emit less generated runtime code Dynamic imports now take an arrow callback instead of a bound `__webpack_require__`, and five runtime helpers drop code an `output.environment` flag or the ES5 baseline already guarantees: - `moduleNamespacePromise` renders `() => (__webpack_require__(id))` where arrows are available; the bound form stays for ES5, where it is shorter. - `__webpack_require__.r` skips the `Symbol.toStringTag` feature test when `output.environment.symbol` is set, which promises it. - `__webpack_require__.g` is `globalThis` when `output.environment.globalThis` is set. - `__webpack_require__.d` builds one descriptor instead of repeating `Object.defineProperty` per branch. - `__webpack_require__.t` reads `Object.getPrototypeOf` directly; its `__proto__` fallback was dead next to `Object.create`. - auto `publicPath` strips the `blob:` prefix and the query/fragment in one pass. Over `test/configCases`, 1937 assets change: -18.85 KiB gzip and -180.22 KiB raw, with error counts and runtime module sets unchanged. * fix(runtime): stop __webpack_require__.O gating every deferred handler `&` binds looser than `===`, so the guard `priority & 1 === 0` parsed as `priority & (1 === 0)` and was always 0. Every deferred handler therefore waited for lower priorities to run first, where `RuntimeGlobals` documents only an odd priority as waiting. Webpack's own output is unaffected: it emits priority 0 (entry startup) and 5 (startup prefetch) only, and for those the two readings agree — `5` is odd, and for `0` the fallback `notFulfilled >= 0` already holds because `notFulfilled` starts at `Infinity` and priorities are documented as `>= 0`. It shows up for a plugin registering an even, non-zero priority, which the new config case does. * perf(runtime): trim more of the generated chunk-loading code - The chunk-filename helper is a concise arrow when no chunk carries a static url, so the common `__webpack_require__.u` is the template expression alone rather than a block with a `return`. - The jsonp loader passes the chunk url straight to `__webpack_require__.l` instead of binding a single-use `const`. - The ESM chunk installer marks a chunk installed through the `chunkId` it already read, not by indexing the id list a second time. - The auto-publicPath regexp drops an escape a character class does not need. `emptyFunction` keeps its named parameter, with a note: `() => {}` reads tidier but minifies a byte longer than `x => {}`, which is why it is there. * test: record the smaller entryB.js in the Stats inline snapshots `Stats.test.js` pins the emitted size of `entryB.js`, which the runtime changes take from 3320 to 3184 bytes. It is a `*.test.js` file, so only the integration matrix runs it — `test:basic` never reaches it. * chore: shorten the changesets and comments this PR adds AGENTS.md caps a changeset description at 80 characters and a comment in `lib/` or `test/` at two short lines; both entries and four comments were over. Flagged in review by coderabbitai. * test: cover `__webpack_require__.r` without Symbol support The `output.environment.symbol: false` branch had no case reaching it: the one es5 target that looked like it would emits no `__webpack_require__.r` at all. A CJS `require` of an ESM module reads `__esModule`, which does. * perf(runtime): use `||=` where `output.environment` allows it Three assignments wrote the ES5 `x = x || y` form unconditionally instead of going through `assignOr`, which picks `||=` when `output.environment.logicalAssignment` is set: the deferred-namespace leaf prototypes, the `__webpack_require__.O` priority default, and the worklet bootstrap blob. Output is byte-identical for a target without the flag. * test: shorten the chunk-url helper comment to two lines * perf(runtime): drop the unused array branch of `__webpack_require__.d` The array form of the define-getters helper is emitted by webpack alone, so a build that emits none cannot receive one and the branch reading it is dead. Gate it on a new `definePropertyGettersFromArray` tree requirement, requested where that form is written. The object form stays unconditionally — third-party code still calls the helper with an object, as its webpack 6 note records. Also drop the empty string a leading or trailing placeholder leaves in a chunk filename expression, keeping a quoted operand so the concatenation still yields a string rather than a bare chunk id. yarn test:size over test/configCases: -69.10 KiB gzip / -267.61 KiB raw over 854 assets; no runtime that both runs build changed its runtime modules. * perf(runtime): drop the unreachable trailing-placeholder strip No chunk filename template in the suite ends with a placeholder while starting with a literal, so the branch stripping the trailing empty string never ran — verified by counting its hits over every configCase under both its own config and the defaults `test:size` builds with. Removing it costs no bytes and leaves the helper with one branch, both outcomes exercised. * style: keep the added comments within two lines
1 parent ce97d58 commit ff27449

58 files changed

Lines changed: 551 additions & 235 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.
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+
Run an even-priority `__webpack_require__.O` handler without waiting.
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+
Emit less runtime code, and shorter syntax where `output.environment` allows.

‎lib/RuntimeGlobals.js‎

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -133,6 +133,11 @@ module.exports.deferredModuleAsyncTransitiveDependenciesSymbol =
133133
*/
134134
module.exports.definePropertyGetters = "__webpack_require__.d";
135135

136+
/**
137+
* a runtime requirement if definePropertyGetters is called with an array of bindings
138+
*/
139+
module.exports.definePropertyGettersFromArray = "__webpack_require__.d (array)";
140+
136141
/**
137142
* the chunk ensure function
138143
*/

‎lib/RuntimePlugin.js‎

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -205,7 +205,6 @@ const TREE_DEPENDENCIES = {
205205
// module to the chunk. Conditional / argument-taking ones stay as explicit taps.
206206
/** @type {[string, () => Promise<new () => RuntimeModule>][]} */
207207
const SIMPLE_TREE_RUNTIME_MODULES = [
208-
[RuntimeGlobals.definePropertyGetters, getDefinePropertyGettersRuntimeModule],
209208
[RuntimeGlobals.makeNamespaceObject, getMakeNamespaceObjectRuntimeModule],
210209
[
211210
RuntimeGlobals.createFakeNamespaceObject,
@@ -314,6 +313,16 @@ class RuntimePlugin {
314313
return true;
315314
});
316315
}
316+
compilation.hooks.runtimeRequirementInTree
317+
.for(RuntimeGlobals.definePropertyGetters)
318+
.tap(PLUGIN_NAME, (chunk, runtimeRequirement) => {
319+
compilation.addLazyRuntimeModule(
320+
chunk,
321+
getDefinePropertyGettersRuntimeModule,
322+
(Ctor) => new Ctor(runtimeRequirement)
323+
);
324+
return true;
325+
});
317326
compilation.hooks.runtimeRequirementInTree
318327
.for(RuntimeGlobals.makeOptimizedDeferredNamespaceObject)
319328
.tap(PLUGIN_NAME, (chunk, runtimeRequirement) => {

‎lib/RuntimeTemplate.js‎

Lines changed: 35 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1291,6 +1291,8 @@ class RuntimeTemplate {
12911291
* @returns {string} empty function code
12921292
*/
12931293
emptyFunction() {
1294+
// `x => {}` over `() => {}`: a minifier keeps the parameter, so the named
1295+
// one is a byte shorter.
12941296
return this.supportsArrowFunction() ? "x => {}" : "function() {}";
12951297
}
12961298

@@ -1324,6 +1326,19 @@ class RuntimeTemplate {
13241326
return `typeof process !== "undefined" && typeof process.getBuiltinModule === "function" && ${getter}`;
13251327
}
13261328

1329+
/**
1330+
* Renders a `then` callback calling `fn` with `args`. An arrow keeps the `this`
1331+
* a method call gives; the bound form is shorter without arrows.
1332+
* @param {string} fn callee, a member of `__webpack_require__` or itself
1333+
* @param {string} args arguments
1334+
* @returns {string} callback expression
1335+
*/
1336+
deferredCall(fn, args) {
1337+
return this.supportsArrowFunction()
1338+
? this.returningFunction(`${fn}(${args})`)
1339+
: `${fn}.bind(${RuntimeGlobals.require}, ${args})`;
1340+
}
1341+
13271342
/**
13281343
* Renders an object-literal method, using method shorthand when supported
13291344
* and falling back to a `prop: function/arrow` property otherwise.
@@ -1829,15 +1844,21 @@ class RuntimeTemplate {
18291844
}(${JSON.stringify(asyncDeps)})`
18301845
)})`;
18311846
}
1832-
appending += `.then(${RuntimeGlobals.makeDeferredNamespaceObject}.bind(${RuntimeGlobals.require}, ${comment}${idExpr}, ${mode}))`;
1847+
appending += `.then(${this.deferredCall(
1848+
RuntimeGlobals.makeDeferredNamespaceObject,
1849+
`${comment}${idExpr}, ${mode}`
1850+
)})`;
18331851
} else if (header) {
18341852
appending = `.then(${this.basicFunction(
18351853
"",
18361854
`${header}return ${RuntimeGlobals.makeDeferredNamespaceObject}(${comment}${idExpr}, ${mode});`
18371855
)})`;
18381856
} else {
18391857
runtimeRequirements.add(RuntimeGlobals.require);
1840-
appending = `.then(${RuntimeGlobals.makeDeferredNamespaceObject}.bind(${RuntimeGlobals.require}, ${comment}${idExpr}, ${mode}))`;
1858+
appending = `.then(${this.deferredCall(
1859+
RuntimeGlobals.makeDeferredNamespaceObject,
1860+
`${comment}${idExpr}, ${mode}`
1861+
)})`;
18411862
}
18421863
} else {
18431864
let fakeType = 16;
@@ -1857,7 +1878,10 @@ class RuntimeTemplate {
18571878
)})`;
18581879
} else {
18591880
runtimeRequirements.add(RuntimeGlobals.require);
1860-
appending = `.then(${RuntimeGlobals.require}.bind(${RuntimeGlobals.require}, ${comment}${idExpr}))`;
1881+
appending = `.then(${this.deferredCall(
1882+
RuntimeGlobals.require,
1883+
`${comment}${idExpr}`
1884+
)})`;
18611885
}
18621886
break;
18631887
case "dynamic":
@@ -1883,7 +1907,10 @@ class RuntimeTemplate {
18831907
)})`;
18841908
} else {
18851909
runtimeRequirements.add(RuntimeGlobals.require);
1886-
appending = `.then(${RuntimeGlobals.require}.bind(${RuntimeGlobals.require}, ${comment}${idExpr}))`;
1910+
appending = `.then(${this.deferredCall(
1911+
RuntimeGlobals.require,
1912+
`${comment}${idExpr}`
1913+
)})`;
18871914
}
18881915
appending += `.then(${this.returningFunction(
18891916
`${RuntimeGlobals.createFakeNamespaceObject}(m, ${fakeType})`,
@@ -1904,7 +1931,10 @@ class RuntimeTemplate {
19041931
`${header}return ${returnExpression};`
19051932
)})`;
19061933
} else {
1907-
appending = `.then(${RuntimeGlobals.createFakeNamespaceObject}.bind(${RuntimeGlobals.require}, ${comment}${idExpr}, ${fakeType}))`;
1934+
appending = `.then(${this.deferredCall(
1935+
RuntimeGlobals.createFakeNamespaceObject,
1936+
`${comment}${idExpr}, ${fakeType}`
1937+
)})`;
19081938
}
19091939
}
19101940
break;

‎lib/dependencies/ExportBindingInitFragment.js‎

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -153,6 +153,9 @@ class ExportBindingInitFragment extends InitFragment {
153153
// emit it, and by then the requirement set is closed.
154154
runtimeRequirements.add(RuntimeGlobals.exports);
155155
runtimeRequirements.add(RuntimeGlobals.definePropertyGetters);
156+
// Only this fragment emits the array form, so the runtime helper drops the
157+
// branch reading it unless this asks for it.
158+
runtimeRequirements.add(RuntimeGlobals.definePropertyGettersFromArray);
156159
const definition = `/* harmony export */ ${
157160
RuntimeGlobals.definePropertyGetters
158161
}(${this.exportsArgument}, [${items.join(",")}\n/* harmony export */ ]);\n`;

‎lib/esm/ModuleChunkLoadingRuntimeModule.js‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -265,7 +265,7 @@ class ModuleChunkLoadingRuntimeModule extends RuntimeModule {
265265
`if(${RuntimeGlobals.hasOwnProperty}(installedChunks, chunkId) && installedChunks[chunkId]) {`,
266266
Template.indent("installedChunks[chunkId][0]();"),
267267
"}",
268-
`installedChunks[${RuntimeGlobals.esmIds}[i]] = 0;`
268+
"installedChunks[chunkId] = 0;"
269269
]),
270270
"}",
271271
withOnChunkLoad ? `${RuntimeGlobals.onChunksLoaded}();` : ""

‎lib/runtime/AutoPublicPathRuntimeModule.js‎

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -85,7 +85,7 @@ class AutoPublicPathRuntimeModule extends RuntimeModule {
8585
"if(scripts.length) {",
8686
Template.indent([
8787
`${lt} i = scripts.length - 1;`,
88-
"while (i > -1 && (!scriptUrl || !/^http(s?):/.test(scriptUrl))) scriptUrl = scripts[i--].src;"
88+
"while (i > -1 && (!scriptUrl || !/^https?:/.test(scriptUrl))) scriptUrl = scripts[i--].src;"
8989
]),
9090
"}"
9191
]),
@@ -96,7 +96,9 @@ class AutoPublicPathRuntimeModule extends RuntimeModule {
9696
"// When supporting browsers where an automatic publicPath is not supported you must specify an output.publicPath manually via configuration",
9797
'// or pass an empty string ("") and set the __webpack_public_path__ variable from your code to use your own logic.',
9898
'if (!scriptUrl) throw new Error("Automatic publicPath is not supported in this browser");',
99-
'scriptUrl = scriptUrl.replace(/^blob:/, "").replace(/#.*$/, "").replace(/\\?.*$/, "").replace(/\\/[^\\/]+$/, "/");',
99+
// One pass for the `blob:` prefix and the query/fragment: both stop at the
100+
// first `?` or `#`, so stripping either first leaves the same url.
101+
'scriptUrl = scriptUrl.replace(/^blob:|[?#].*$/g, "").replace(/\\/[^/]+$/, "/");',
100102
!undoPath
101103
? `${RuntimeGlobals.publicPath} = scriptUrl;`
102104
: `${RuntimeGlobals.publicPath} = scriptUrl + ${JSON.stringify(

‎lib/runtime/CreateFakeNamespaceObjectRuntimeModule.js‎

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -26,10 +26,9 @@ class CreateFakeNamespaceObjectRuntimeModule extends HelperRuntimeModule {
2626
const cst = runtimeTemplate.renderConst();
2727
const lt = runtimeTemplate.renderLet();
2828
return Template.asString([
29-
`${cst} getProto = Object.getPrototypeOf ? ${runtimeTemplate.returningFunction(
30-
"Object.getPrototypeOf(obj)",
31-
"obj"
32-
)} : ${runtimeTemplate.returningFunction("obj.__proto__", "obj")};`,
29+
// No `__proto__` fallback: `Object.create` and `Object.defineProperty`
30+
// below already require the same ES5 baseline.
31+
`${cst} getProto = Object.getPrototypeOf;`,
3332
`${lt} leafPrototypes;`,
3433
"// create a fake namespace object",
3534
"// mode & 1: value is a module id, require it",
@@ -51,7 +50,10 @@ class CreateFakeNamespaceObjectRuntimeModule extends HelperRuntimeModule {
5150
`${cst} ns = Object.create(null);`,
5251
`${RuntimeGlobals.makeNamespaceObject}(ns);`,
5352
`${cst} def = {};`,
54-
"leafPrototypes = leafPrototypes || [null, getProto({}), getProto([]), getProto(getProto)];",
53+
`${runtimeTemplate.assignOr(
54+
"leafPrototypes",
55+
"[null, getProto({}), getProto([]), getProto(getProto)]"
56+
)};`,
5557
"for(var current = mode & 2 && value; (typeof current == 'object' || typeof current == 'function') && !~leafPrototypes.indexOf(current); current = getProto(current)) {",
5658
Template.indent([
5759
`Object.getOwnPropertyNames(current).forEach(${runtimeTemplate.expressionFunction(

‎lib/runtime/DefinePropertyGettersRuntimeModule.js‎

Lines changed: 45 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -9,10 +9,16 @@ const Template = require("../Template");
99
const HelperRuntimeModule = require("./HelperRuntimeModule");
1010

1111
/** @import Compilation from "../Compilation" */
12+
/** @import { ReadOnlyRuntimeRequirements } from "../Module" */
1213

1314
class DefinePropertyGettersRuntimeModule extends HelperRuntimeModule {
14-
constructor() {
15+
/**
16+
* @param {ReadOnlyRuntimeRequirements} runtimeRequirements runtime requirements
17+
*/
18+
constructor(runtimeRequirements) {
1519
super("define property getters");
20+
/** @type {ReadOnlyRuntimeRequirements} */
21+
this.runtimeRequirements = runtimeRequirements;
1622
}
1723

1824
/**
@@ -33,48 +39,50 @@ class DefinePropertyGettersRuntimeModule extends HelperRuntimeModule {
3339
const compilation = /** @type {Compilation} */ (this.compilation);
3440
const { runtimeTemplate } = compilation;
3541
const fn = RuntimeGlobals.definePropertyGetters;
42+
// Only webpack emits the array form, so a build emitting none cannot
43+
// receive one; the object form stays either way (see the note below).
44+
const withArray = this.runtimeRequirements.has(
45+
RuntimeGlobals.definePropertyGettersFromArray
46+
);
47+
// TODO webpack 6: remove object format support. Internal code (e.g. ConcatenatedModule)
48+
// and third-party libraries may still call __webpack_require__.d() with an object.
49+
const fromObject = [
50+
"for(var key in definition) {",
51+
Template.indent([
52+
`if(${RuntimeGlobals.hasOwnProperty}(definition, key) && !${RuntimeGlobals.hasOwnProperty}(exports, key)) {`,
53+
Template.indent([
54+
"Object.defineProperty(exports, key, { enumerable: true, get: definition[key] });"
55+
]),
56+
"}"
57+
]),
58+
"}"
59+
];
3660
return Template.asString([
3761
"// define getter/value functions for harmony exports",
38-
`${fn} = ${runtimeTemplate.basicFunction("exports, definition", [
39-
"if(Array.isArray(definition)) {",
40-
Template.indent([
41-
"var i = 0;",
42-
"while(i < definition.length) {",
43-
Template.indent([
44-
"var key = definition[i++];",
45-
"var binding = definition[i++];",
46-
`if(!${RuntimeGlobals.hasOwnProperty}(exports, key)) {`,
47-
Template.indent([
48-
"if(binding === 0) {",
62+
`${fn} = ${runtimeTemplate.basicFunction(
63+
"exports, definition",
64+
withArray
65+
? [
66+
"if(Array.isArray(definition)) {",
4967
Template.indent([
50-
"Object.defineProperty(exports, key, { enumerable: true, value: definition[i++] });"
68+
"var i = 0;",
69+
"while(i < definition.length) {",
70+
Template.indent([
71+
"var key = definition[i++];",
72+
"var binding = definition[i++];",
73+
// The descriptor is built before the own-property check so a
74+
// value binding consumes its slot either way.
75+
"var descriptor = binding === 0 ? { enumerable: true, value: definition[i++] } : { enumerable: true, get: binding };",
76+
`if(!${RuntimeGlobals.hasOwnProperty}(exports, key)) Object.defineProperty(exports, key, descriptor);`
77+
]),
78+
"}"
5179
]),
5280
"} else {",
53-
Template.indent([
54-
"Object.defineProperty(exports, key, { enumerable: true, get: binding });"
55-
]),
81+
Template.indent(fromObject),
5682
"}"
57-
]),
58-
"} else if(binding === 0) { i++; }"
59-
]),
60-
"}"
61-
]),
62-
// TODO webpack 6: remove object format support. Internal code (e.g. ConcatenatedModule)
63-
// and third-party libraries may still call __webpack_require__.d() with an object.
64-
"} else {",
65-
Template.indent([
66-
"for(var key in definition) {",
67-
Template.indent([
68-
`if(${RuntimeGlobals.hasOwnProperty}(definition, key) && !${RuntimeGlobals.hasOwnProperty}(exports, key)) {`,
69-
Template.indent([
70-
"Object.defineProperty(exports, key, { enumerable: true, get: definition[key] });"
71-
]),
72-
"}"
73-
]),
74-
"}"
75-
]),
76-
"}"
77-
])};`
83+
]
84+
: fromObject
85+
)};`
7886
]);
7987
}
8088
}

0 commit comments

Comments
 (0)