Skip to content

Commit 7f5239e

Browse files
ScriptedAlchemymitchellrj
authored andcommitted
perf: add tree shake markers to enable/disable capabilities to reduce bundle size (#3704)
1 parent 8857898 commit 7f5239e

File tree

13 files changed

+394
-180
lines changed

13 files changed

+394
-180
lines changed

.changeset/ai-calm-dog.md

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
---
2+
"@module-federation/enhanced": patch
3+
---
4+
5+
Updated ModuleFederationPlugin to enhance configuration capabilities and target environment identification.
6+
7+
- Introduced `definePluginOptions` to manage DefinePlugin settings.
8+
- Added `FEDERATION_OPTIMIZE_NO_SNAPSHOT_PLUGIN` to handle disabling of snapshot optimizations via experiments.
9+
- Implemented environment target detection (`web` or `node`) based on compiler options and experiments.
10+
- Consolidated DefinePlugin application with the newly constructed `definePluginOptions`.

.changeset/ai-sleepy-cat.md

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
---
2+
"@module-federation/sdk": patch
3+
---
4+
5+
Introduced environment-specific handling for `createScriptNode` and `loadScriptNode` functions and added build optimization options.
6+
7+
- Declared `ENV_TARGET` constant to differentiate between 'web' and 'node' environments.
8+
- Modified `createScriptNode` and `loadScriptNode` to execute only in Node.js environment.
9+
- Throws an error if attempted in a non-Node.js environment.
10+
- Added logging for debugging purposes.
11+
- Introduced `optimization` options in `ModuleFederationPluginOptions`.
12+
- Added config for `disableSnapshot` and `target` environment optimizations.

.changeset/ai-sleepy-lion.md

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
---
2+
"@module-federation/runtime-core": patch
3+
---
4+
5+
Add conditional functionality for snapshots and optimize entry loading.
6+
7+
- Introduced FEDERATION_OPTIMIZE_NO_SNAPSHOT_PLUGIN constant to control snapshot functionality.
8+
- Default to include snapshot functionality if constant is not defined.
9+
- Simplified plugin loading logic to check USE_SNAPSHOT flag.
10+
- Added ENV_TARGET constant to differentiate between web and node environments.
11+
- Extracted duplicated logic for handling remote entry loaded into `handleRemoteEntryLoaded` function.
12+
- Refactored entry loading to use conditional environment checks with `ENV_TARGET`.
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
---
2+
"@module-federation/rspack": patch
3+
---
4+
5+
Update Rspack ModuleFederationPlugin to support enhanced configuration capabilities and environment targeting.
6+
7+
- Injects `FEDERATION_OPTIMIZE_NO_SNAPSHOT_PLUGIN` and `ENV_TARGET` as global constants using DefinePlugin, based on the new `experiments.optimization` options.
8+
- Ensures parity with the Webpack plugin for build-time optimizations and environment-specific code paths.
9+
- Enables tree-shaking and feature toggling in the runtime and SDK for both Rspack and Webpack builds.

apps/website-new/docs/en/configure/experiments.mdx

Lines changed: 42 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,11 @@ new ModuleFederationPlugin({
1010
experiments: {
1111
asyncStartup: true,
1212
externalRuntime: false,
13-
provideExternalRuntime: false
13+
provideExternalRuntime: false,
14+
optimization: {
15+
disableSnapshot: false,
16+
target: 'web',
17+
},
1418
},
1519
shared: {
1620
react: {
@@ -59,3 +63,40 @@ Make sure to only configure it on the topmost consumer! If multiple consumers in
5963
:::
6064

6165
Setting `true` will inject the MF runtime at the consumer.
66+
67+
## optimization
68+
69+
This object contains flags related to build-time optimizations that can affect the Module Federation runtime's size and behavior.
70+
71+
### disableSnapshot
72+
73+
- Type: `boolean`
74+
- Required: No
75+
- Default: `false`
76+
77+
When set to `true`, this option defines the `FEDERATION_OPTIMIZE_NO_SNAPSHOT_PLUGIN` global constant as `true` during the build. In the `@module-federation/runtime-core`, this prevents the `snapshotPlugin()` and `generatePreloadAssetsPlugin()` from being included and initialized within the FederationHost.
78+
79+
**Impact:**
80+
* **Benefit:** Can reduce the overall bundle size of the Module Federation runtime by excluding the code for these two plugins.
81+
* **Cost:** Disables the functionality provided by these plugins. The `snapshotPlugin` is crucial for the "mf-manifest protocol" – it's responsible for generating or providing runtime access to a build manifest (e.g., `mf-manifest.json`) containing metadata about exposed modules, shared dependencies, versions, and remotes. Disabling it means:
82+
* The runtime loses access to this build manifest data.
83+
* Features relying on the manifest, such as dynamic remote discovery, manifest-based version compatibility checks, advanced asset preloading (also handled by the removed `generatePreloadAssetsPlugin`), and potentially runtime debugging/introspection tools, will not function correctly or will be unavailable.
84+
* Use this option only if you do not rely on these manifest-driven features and prioritize a minimal runtime footprint.
85+
86+
### target
87+
88+
- Type: `'web' | 'node'`
89+
- Required: No
90+
- Default: Inferred from Webpack's `target` option (usually `'web'`)
91+
92+
This option defines the `ENV_TARGET` global constant during the build, specifying the intended execution environment.
93+
94+
**Impact:**
95+
* **`target: 'web'`**: Optimizes the build for browser environments.
96+
* Ensures browser-specific remote entry loading mechanisms are used (`loadEntryDom`).
97+
* Crucially, enables tree-shaking/dead-code elimination for Node.js-specific code within the `@module-federation/sdk`. Functions like `createScriptNode` and `loadScriptNode`, along with their required Node.js built-in modules (e.g., `vm`, `path`, `http`), are completely removed from the bundle, significantly reducing its size.
98+
* **`target: 'node'`**: Optimizes the build for Node.js environments.
99+
* Ensures Node.js-specific remote entry loading mechanisms are used (`loadEntryNode`).
100+
* Includes the necessary Node.js-specific functions from the SDK (`createScriptNode`, `loadScriptNode`) in the bundle, allowing the federated application to function correctly in Node.js.
101+
102+
Explicitly setting this value is recommended to ensure the intended optimizations are applied, especially in universal or server-side rendering scenarios.

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,7 @@
5151
"prepare": "husky install",
5252
"changeset": "changeset",
5353
"build:packages": "npx nx affected -t build --parallel=10 --exclude='*,!tag:type:pkg'",
54-
"changegen": "./changeset-gen.js --path ./packages/enhanced --staged &&./changeset-gen.js --path ./packages/cli --staged && ./changeset-gen.js --path ./packages/node --staged && ./changeset-gen.js --path ./packages/runtime --staged && ./changeset-gen.js --path ./packages/data-prefetch --staged && ./changeset-gen.js --path ./packages/nextjs-mf --staged && ./changeset-gen.js --path ./packages/dts-plugin --staged",
54+
"changegen": "./changeset-gen.js --path ./packages/runtime && ./changeset-gen.js --path ./packages/runtime-core && ./changeset-gen.js --path ./packages/sdk &&./changeset-gen.js --path ./packages/cli --staged && ./changeset-gen.js --path ./packages/enhanced && ./changeset-gen.js --path ./packages/node && ./changeset-gen.js --path ./packages/data-prefetch && ./changeset-gen.js --path ./packages/nextjs-mf && ./changeset-gen.js --path ./packages/dts-plugin",
5555
"commitgen:staged": "./commit-gen.js --path ./packages --staged",
5656
"commitgen:main": "./commit-gen.js --path ./packages",
5757
"changeset:status": "changeset status",

packages/chrome-devtools/project.json

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,13 @@
2121
"executor": "nx:run-commands",
2222
"options": {
2323
"commands": ["npm run build --prefix packages/chrome-devtools"]
24-
}
24+
},
25+
"dependsOn": [
26+
{
27+
"target": "build",
28+
"dependencies": true
29+
}
30+
]
2531
},
2632
"test": {
2733
"executor": "nx:run-commands",

packages/enhanced/src/lib/container/ModuleFederationPlugin.ts

Lines changed: 31 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -57,18 +57,43 @@ class ModuleFederationPlugin implements WebpackPluginInstance {
5757
}
5858

5959
private _patchBundlerConfig(compiler: Compiler): void {
60-
const { name } = this._options;
60+
const { name, experiments } = this._options;
61+
const definePluginOptions: Record<string, string | boolean> = {};
62+
6163
const MFPluginNum = compiler.options.plugins.filter(
6264
(p): p is WebpackPluginInstance =>
6365
!!p && (p as any).name === 'ModuleFederationPlugin',
6466
).length;
67+
6568
if (name && MFPluginNum < 2) {
66-
new compiler.webpack.DefinePlugin({
67-
FEDERATION_BUILD_IDENTIFIER: JSON.stringify(
68-
composeKeyWithSeparator(name, utils.getBuildVersion()),
69-
),
70-
}).apply(compiler);
69+
definePluginOptions['FEDERATION_BUILD_IDENTIFIER'] = JSON.stringify(
70+
composeKeyWithSeparator(name, utils.getBuildVersion()),
71+
);
72+
}
73+
74+
const disableSnapshot = experiments?.optimization?.disableSnapshot ?? false;
75+
definePluginOptions['FEDERATION_OPTIMIZE_NO_SNAPSHOT_PLUGIN'] =
76+
disableSnapshot;
77+
78+
// Determine ENV_TARGET: only if manually specified in experiments.optimization.target
79+
if (
80+
experiments?.optimization &&
81+
typeof experiments.optimization === 'object' &&
82+
experiments.optimization !== null &&
83+
'target' in experiments.optimization
84+
) {
85+
const manualTarget = experiments.optimization.target as
86+
| 'web'
87+
| 'node'
88+
| undefined;
89+
// Ensure the target is one of the expected values before setting
90+
if (manualTarget === 'web' || manualTarget === 'node') {
91+
definePluginOptions['ENV_TARGET'] = JSON.stringify(manualTarget);
92+
}
7193
}
94+
// No inference for ENV_TARGET. If not manually set and valid, it's not defined.
95+
96+
new compiler.webpack.DefinePlugin(definePluginOptions).apply(compiler);
7297
}
7398

7499
/**

packages/rspack/src/ModuleFederationPlugin.ts

Lines changed: 29 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -39,14 +39,37 @@ export class ModuleFederationPlugin implements RspackPluginInstance {
3939
}
4040

4141
private _patchBundlerConfig(compiler: Compiler): void {
42-
const { name } = this._options;
42+
const { name, experiments } = this._options;
43+
const definePluginOptions: Record<string, string | boolean> = {};
4344
if (name) {
44-
new compiler.webpack.DefinePlugin({
45-
FEDERATION_BUILD_IDENTIFIER: JSON.stringify(
46-
composeKeyWithSeparator(name, utils.getBuildVersion()),
47-
),
48-
}).apply(compiler);
45+
definePluginOptions['FEDERATION_BUILD_IDENTIFIER'] = JSON.stringify(
46+
composeKeyWithSeparator(name, utils.getBuildVersion()),
47+
);
4948
}
49+
// Add FEDERATION_OPTIMIZE_NO_SNAPSHOT_PLUGIN
50+
const disableSnapshot = experiments?.optimization?.disableSnapshot ?? false;
51+
definePluginOptions['FEDERATION_OPTIMIZE_NO_SNAPSHOT_PLUGIN'] =
52+
disableSnapshot;
53+
54+
// Determine ENV_TARGET: only if manually specified in experiments.optimization.target
55+
if (
56+
experiments?.optimization &&
57+
typeof experiments.optimization === 'object' &&
58+
experiments.optimization !== null &&
59+
'target' in experiments.optimization
60+
) {
61+
const manualTarget = experiments.optimization.target as
62+
| 'web'
63+
| 'node'
64+
| undefined;
65+
// Ensure the target is one of the expected values before setting
66+
if (manualTarget === 'web' || manualTarget === 'node') {
67+
definePluginOptions['ENV_TARGET'] = JSON.stringify(manualTarget);
68+
}
69+
}
70+
// No inference for ENV_TARGET. If not manually set and valid, it's not defined.
71+
72+
new compiler.webpack.DefinePlugin(definePluginOptions).apply(compiler);
5073
}
5174

5275
private _checkSingleton(compiler: Compiler): void {

packages/runtime-core/src/core.ts

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,15 @@ import { SharedHandler } from './shared';
3535
import { RemoteHandler } from './remote';
3636
import { formatShareConfigs } from './utils/share';
3737

38+
// Declare the global constant that will be defined by DefinePlugin
39+
// Default to true if not defined (e.g., when runtime-core is used outside of webpack)
40+
// so that snapshot functionality is included by default.
41+
declare const FEDERATION_OPTIMIZE_NO_SNAPSHOT_PLUGIN: boolean;
42+
const USE_SNAPSHOT =
43+
typeof FEDERATION_OPTIMIZE_NO_SNAPSHOT_PLUGIN === 'boolean'
44+
? !FEDERATION_OPTIMIZE_NO_SNAPSHOT_PLUGIN
45+
: true; // Default to true (use snapshot) when not explicitly defined
46+
3847
export class FederationHost {
3948
options: Options;
4049
hooks = new PluginSystem({
@@ -160,12 +169,15 @@ export class FederationHost {
160169
});
161170

162171
constructor(userOptions: UserOptions) {
172+
const plugins = USE_SNAPSHOT
173+
? [snapshotPlugin(), generatePreloadAssetsPlugin()]
174+
: [];
163175
// TODO: Validate the details of the options
164176
// Initialize options with default values
165177
const defaultOptions: Options = {
166178
id: getBuilderId(),
167179
name: userOptions.name,
168-
plugins: [snapshotPlugin(), generatePreloadAssetsPlugin()],
180+
plugins,
169181
remotes: [],
170182
shared: {},
171183
inBrowser: isBrowserEnv(),
@@ -328,7 +340,6 @@ export class FederationHost {
328340
return res;
329341
}, pluginRes || []);
330342
}
331-
332343
registerRemotes(remotes: Remote[], options?: { force?: boolean }): void {
333344
return this.remoteHandler.registerRemotes(remotes, options);
334345
}

packages/runtime-core/src/utils/load.ts

Lines changed: 40 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,10 @@ import {
1616
runtimeDescMap,
1717
} from '@module-federation/error-codes';
1818

19+
// Declare the ENV_TARGET constant that will be defined by DefinePlugin
20+
declare const ENV_TARGET: 'web' | 'node';
21+
const importCallback = '.then(callbacks[0]).catch(callbacks[1])';
22+
1923
async function loadEsmEntry({
2024
entry,
2125
remoteEntryExports,
@@ -27,10 +31,10 @@ async function loadEsmEntry({
2731
try {
2832
if (!remoteEntryExports) {
2933
if (typeof FEDERATION_ALLOW_NEW_FUNCTION !== 'undefined') {
30-
new Function(
31-
'callbacks',
32-
`import("${entry}").then(callbacks[0]).catch(callbacks[1])`,
33-
)([resolve, reject]);
34+
new Function('callbacks', `import("${entry}")${importCallback}`)([
35+
resolve,
36+
reject,
37+
]);
3438
} else {
3539
import(/* webpackIgnore: true */ /* @vite-ignore */ entry)
3640
.then(resolve)
@@ -62,7 +66,7 @@ async function loadSystemJsEntry({
6266
} else {
6367
new Function(
6468
'callbacks',
65-
`System.import("${entry}").then(callbacks[0]).catch(callbacks[1])`,
69+
`System.import("${entry}")${importCallback}`,
6670
)([resolve, reject]);
6771
}
6872
} else {
@@ -74,6 +78,28 @@ async function loadSystemJsEntry({
7478
});
7579
}
7680

81+
function handleRemoteEntryLoaded(
82+
name: string,
83+
globalName: string,
84+
entry: string,
85+
): RemoteEntryExports {
86+
const { remoteEntryKey, entryExports } = getRemoteEntryExports(
87+
name,
88+
globalName,
89+
);
90+
91+
assert(
92+
entryExports,
93+
getShortErrorMsg(RUNTIME_001, runtimeDescMap, {
94+
remoteName: name,
95+
remoteEntryUrl: entry,
96+
remoteEntryKey,
97+
}),
98+
);
99+
100+
return entryExports;
101+
}
102+
77103
async function loadEntryScript({
78104
name,
79105
globalName,
@@ -113,21 +139,7 @@ async function loadEntryScript({
113139
},
114140
})
115141
.then(() => {
116-
const { remoteEntryKey, entryExports } = getRemoteEntryExports(
117-
name,
118-
globalName,
119-
);
120-
121-
assert(
122-
entryExports,
123-
getShortErrorMsg(RUNTIME_001, runtimeDescMap, {
124-
remoteName: name,
125-
remoteEntryUrl: entry,
126-
remoteEntryKey,
127-
}),
128-
);
129-
130-
return entryExports;
142+
return handleRemoteEntryLoaded(name, globalName, entry);
131143
})
132144
.catch((e) => {
133145
assert(
@@ -196,21 +208,7 @@ async function loadEntryNode({
196208
},
197209
})
198210
.then(() => {
199-
const { remoteEntryKey, entryExports } = getRemoteEntryExports(
200-
name,
201-
globalName,
202-
);
203-
204-
assert(
205-
entryExports,
206-
getShortErrorMsg(RUNTIME_001, runtimeDescMap, {
207-
remoteName: name,
208-
remoteEntryUrl: entry,
209-
remoteEntryKey,
210-
}),
211-
);
212-
213-
return entryExports;
211+
return handleRemoteEntryLoaded(name, globalName, entry);
214212
})
215213
.catch((e) => {
216214
throw e;
@@ -250,7 +248,13 @@ export async function getRemoteEntry({
250248
if (res) {
251249
return res;
252250
}
253-
return isBrowserEnv()
251+
// Use ENV_TARGET if defined, otherwise fallback to isBrowserEnv, must keep this
252+
const isWebEnvironment =
253+
typeof ENV_TARGET !== 'undefined'
254+
? ENV_TARGET === 'web'
255+
: isBrowserEnv();
256+
257+
return isWebEnvironment
254258
? loadEntryDom({ remoteInfo, remoteEntryExports, loaderHook })
255259
: loadEntryNode({ remoteInfo, loaderHook });
256260
});

0 commit comments

Comments
 (0)