Skip to content

perf: add tree shake markers to enable/disable capabilities to reduce bundle size #3704

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 12 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions .changeset/ai-calm-dog.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
---
"@module-federation/enhanced": patch
---

Updated ModuleFederationPlugin to enhance configuration capabilities and target environment identification.

- Introduced `definePluginOptions` to manage DefinePlugin settings.
- Added `FEDERATION_OPTIMIZE_NO_SNAPSHOT_PLUGIN` to handle disabling of snapshot optimizations via experiments.
- Implemented environment target detection (`web` or `node`) based on compiler options and experiments.
- Consolidated DefinePlugin application with the newly constructed `definePluginOptions`.
13 changes: 13 additions & 0 deletions .changeset/ai-sleepy-cat.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
---
"@module-federation/sdk": patch
---

Introduced environment-specific handling for `createScriptNode` and `loadScriptNode` functions and added build optimization options.

- Declared `ENV_TARGET` constant to differentiate between 'web' and 'node' environments.
- Modified `createScriptNode` and `loadScriptNode` to execute only in Node.js environment.
- Throws an error if attempted in a non-Node.js environment.
- Added logging for debugging purposes.
- Introduced `optimization` options in `ModuleFederationPluginOptions`.
- Added config for `disableSnapshot` and `target` environment optimizations.
```
12 changes: 12 additions & 0 deletions .changeset/ai-sleepy-lion.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
"@module-federation/runtime-core": patch
---

Add conditional functionality for snapshots and optimize entry loading.

- Introduced FEDERATION_OPTIMIZE_NO_SNAPSHOT_PLUGIN constant to control snapshot functionality.
- Default to include snapshot functionality if constant is not defined.
- Simplified plugin loading logic to check USE_SNAPSHOT flag.
- Added ENV_TARGET constant to differentiate between web and node environments.
- Extracted duplicated logic for handling remote entry loaded into `handleRemoteEntryLoaded` function.
- Refactored entry loading to use conditional environment checks with `ENV_TARGET`.
```
9 changes: 9 additions & 0 deletions .changeset/rspack-define-env-constants.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
---
"@module-federation/rspack": patch
---

Update Rspack ModuleFederationPlugin to support enhanced configuration capabilities and environment targeting.

- Injects `FEDERATION_OPTIMIZE_NO_SNAPSHOT_PLUGIN` and `ENV_TARGET` as global constants using DefinePlugin, based on the new `experiments.optimization` options.
- Ensures parity with the Webpack plugin for build-time optimizations and environment-specific code paths.
- Enables tree-shaking and feature toggling in the runtime and SDK for both Rspack and Webpack builds.
43 changes: 42 additions & 1 deletion apps/website-new/docs/en/configure/experiments.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,11 @@ new ModuleFederationPlugin({
experiments: {
asyncStartup: true,
externalRuntime: false,
provideExternalRuntime: false
provideExternalRuntime: false,
optimization: {
disableSnapshot: false,
target: 'web',
},
},
shared: {
react: {
Expand Down Expand Up @@ -59,3 +63,40 @@ Make sure to only configure it on the topmost consumer! If multiple consumers in
:::

Setting `true` will inject the MF runtime at the consumer.

## optimization

This object contains flags related to build-time optimizations that can affect the Module Federation runtime's size and behavior.

### disableSnapshot

- Type: `boolean`
- Required: No
- Default: `false`

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.

**Impact:**
* **Benefit:** Can reduce the overall bundle size of the Module Federation runtime by excluding the code for these two plugins.
* **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:
* The runtime loses access to this build manifest data.
* 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.
* Use this option only if you do not rely on these manifest-driven features and prioritize a minimal runtime footprint.

### target

- Type: `'web' | 'node'`
- Required: No
- Default: Inferred from Webpack's `target` option (usually `'web'`)

This option defines the `ENV_TARGET` global constant during the build, specifying the intended execution environment.

**Impact:**
* **`target: 'web'`**: Optimizes the build for browser environments.
* Ensures browser-specific remote entry loading mechanisms are used (`loadEntryDom`).
* 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.
* **`target: 'node'`**: Optimizes the build for Node.js environments.
* Ensures Node.js-specific remote entry loading mechanisms are used (`loadEntryNode`).
* 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.

Explicitly setting this value is recommended to ensure the intended optimizations are applied, especially in universal or server-side rendering scenarios.
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@
"prepare": "husky install",
"changeset": "changeset",
"build:packages": "npx nx affected -t build --parallel=10 --exclude='*,!tag:type:pkg'",
"changegen": "./changeset-gen.js --path ./packages/enhanced --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",
"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/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",
"commitgen:staged": "./commit-gen.js --path ./packages --staged",
"commitgen:main": "./commit-gen.js --path ./packages",
"changeset:status": "changeset status",
Expand Down
16 changes: 14 additions & 2 deletions packages/chrome-devtools/project.json
Original file line number Diff line number Diff line change
Expand Up @@ -9,13 +9,25 @@
"executor": "nx:run-commands",
"options": {
"commands": ["npm run build:lib --prefix packages/chrome-devtools"]
}
},
"dependsOn": [
{
"target": "build",
"dependencies": true
}
]
},
"build:chrome-plugins": {
"executor": "nx:run-commands",
"options": {
"commands": ["npm run build --prefix packages/chrome-devtools"]
}
},
"dependsOn": [
{
"target": "build",
"dependencies": true
}
]
},
"test": {
"executor": "nx:run-commands",
Expand Down
39 changes: 33 additions & 6 deletions packages/enhanced/src/lib/container/ModuleFederationPlugin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -57,18 +57,45 @@ class ModuleFederationPlugin implements WebpackPluginInstance {
}

private _patchBundlerConfig(compiler: Compiler): void {
const { name } = this._options;
const { name, experiments } = this._options;
const definePluginOptions: Record<string, string | boolean> = {};

const MFPluginNum = compiler.options.plugins.filter(
(p): p is WebpackPluginInstance =>
!!p && (p as any).name === 'ModuleFederationPlugin',
).length;

if (name && MFPluginNum < 2) {
new compiler.webpack.DefinePlugin({
FEDERATION_BUILD_IDENTIFIER: JSON.stringify(
composeKeyWithSeparator(name, utils.getBuildVersion()),
),
}).apply(compiler);
definePluginOptions['FEDERATION_BUILD_IDENTIFIER'] = JSON.stringify(
composeKeyWithSeparator(name, utils.getBuildVersion()),
);
}

const disableSnapshot = experiments?.optimization?.disableSnapshot ?? false;
definePluginOptions['FEDERATION_OPTIMIZE_NO_SNAPSHOT_PLUGIN'] =
disableSnapshot;

let targetEnv: 'web' | 'node' | undefined;
if (experiments?.optimization && 'target' in experiments.optimization) {
targetEnv = experiments.optimization.target;
} else {
targetEnv = 'web';
const webpackTarget = compiler.options.target;
if (typeof webpackTarget === 'string') {
if (webpackTarget.includes('node')) {
targetEnv = 'node';
}
} else if (Array.isArray(webpackTarget)) {
if (
webpackTarget.some((t) => typeof t === 'string' && t.includes('node'))
) {
targetEnv = 'node';
}
}
}
definePluginOptions['ENV_TARGET'] = JSON.stringify(targetEnv);

new compiler.webpack.DefinePlugin(definePluginOptions).apply(compiler);
}

/**
Expand Down
38 changes: 32 additions & 6 deletions packages/rspack/src/ModuleFederationPlugin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,14 +37,40 @@ export class ModuleFederationPlugin implements RspackPluginInstance {
}

private _patchBundlerConfig(compiler: Compiler): void {
const { name } = this._options;
const { name, experiments } = this._options;
const definePluginOptions: Record<string, string | boolean> = {};
if (name) {
new compiler.webpack.DefinePlugin({
FEDERATION_BUILD_IDENTIFIER: JSON.stringify(
composeKeyWithSeparator(name, utils.getBuildVersion()),
),
}).apply(compiler);
definePluginOptions['FEDERATION_BUILD_IDENTIFIER'] = JSON.stringify(
composeKeyWithSeparator(name, utils.getBuildVersion()),
);
}
// Add FEDERATION_OPTIMIZE_NO_SNAPSHOT_PLUGIN
const disableSnapshot = experiments?.optimization?.disableSnapshot ?? false;
definePluginOptions['FEDERATION_OPTIMIZE_NO_SNAPSHOT_PLUGIN'] =
disableSnapshot;

// Add ENV_TARGET
let targetEnv: 'web' | 'node' | undefined;
if (experiments?.optimization && 'target' in experiments.optimization) {
targetEnv = experiments.optimization.target;
} else {
targetEnv = 'web';
const rspackTarget = compiler.options.target;
if (typeof rspackTarget === 'string') {
if (rspackTarget.includes('node')) {
targetEnv = 'node';
}
} else if (Array.isArray(rspackTarget)) {
if (
rspackTarget.some((t) => typeof t === 'string' && t.includes('node'))
) {
targetEnv = 'node';
}
}
}
definePluginOptions['ENV_TARGET'] = JSON.stringify(targetEnv);

new compiler.webpack.DefinePlugin(definePluginOptions).apply(compiler);
}

private _checkSingleton(compiler: Compiler): void {
Expand Down
15 changes: 13 additions & 2 deletions packages/runtime-core/src/core.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,15 @@ import { SharedHandler } from './shared';
import { RemoteHandler } from './remote';
import { formatShareConfigs } from './utils/share';

// Declare the global constant that will be defined by DefinePlugin
// Default to true if not defined (e.g., when runtime-core is used outside of webpack)
// so that snapshot functionality is included by default.
declare const FEDERATION_OPTIMIZE_NO_SNAPSHOT_PLUGIN: boolean;
const USE_SNAPSHOT =
typeof FEDERATION_OPTIMIZE_NO_SNAPSHOT_PLUGIN === 'boolean'
? !FEDERATION_OPTIMIZE_NO_SNAPSHOT_PLUGIN
: true;

export class FederationHost {
options: Options;
hooks = new PluginSystem({
Expand Down Expand Up @@ -160,12 +169,15 @@ export class FederationHost {
});

constructor(userOptions: UserOptions) {
const plugins = USE_SNAPSHOT
? [snapshotPlugin(), generatePreloadAssetsPlugin()]
: [];
// TODO: Validate the details of the options
// Initialize options with default values
const defaultOptions: Options = {
id: getBuilderId(),
name: userOptions.name,
plugins: [snapshotPlugin(), generatePreloadAssetsPlugin()],
plugins,
remotes: [],
shared: {},
inBrowser: isBrowserEnv(),
Expand Down Expand Up @@ -328,7 +340,6 @@ export class FederationHost {
return res;
}, pluginRes || []);
}

registerRemotes(remotes: Remote[], options?: { force?: boolean }): void {
return this.remoteHandler.registerRemotes(remotes, options);
}
Expand Down
Loading
Loading