Module Federation gets upgraded, and Rspack supports it.

Zack Jackson
9 min readJan 9, 2024

--

Its a double whammy release for the Module Federation universe. Four years after its creation, it gets a major upgrade.

1) Module Federation Group introduces the next iteration, introducing runtime hooks, apis, SDK, decoupling itself from webpack.
2) Rspack releases 0.5.0, which includes Module Federation.

Webpack Users can use the enhanced Webpack Plugin
Rspack users, its built into 0.5.0

Rspack Support

Migration Opportunities

The support of Module Federation in Rspack opens up a several of creative migration options to speed up bundler tools by sharing code at runtime. Both Webpack and Rspack can share code, relying on the same centralized runtime that the Module Federation Group introduced in the update. This ensures maintaining feature parity is manageable, and no additional forks of Module Federation are necessary to customize it.

Progressive migration to rspack can be achieved via federation. If you have webpack locked plugins or cannot perform a full cut over to rspack, via module federation you can allow rspack and webpack to share dependencies and code, meaning more code could be built via rspack while the webpack host does less work but still gets the same result. example: Webpack Rspack interop

Speed up builds by sharing the node_modules via federation. One could tell webpack to import: false them, and rspack could compile all the shared modules, reducing the parse overhead and amount of code the webpack part has to do, by delegating it to rspack where similar workloads take only a few milliseconds to perform. example: Rspack Vendor Offload to Webpack apps

Migrate one at a time. Since the interfaces between webpack (@module-federation/enhanced) and rspack are shared, users can switch over any existing federation build or remote to rspack. We recommend any remaining webpack builds using @module-federation/enhanced which leverages our new design and exports ModuleFederationPlugin. You can, however, still use the stock plugin that ships in webpack core. Rspack should slot in seamlessly with existing federated applications.

Webpack Federation gets some love!

Federation API has been opened up for users to enrich, expand, or manage the lifecycle. While v1.5 comes with several new capabilities, it doesn’t introduce breaking changes to the API regarding the original Module Federation.

The upgrade is also accessible to webpack via @module-federation/enhanced with the upper plugin ecosystem, such as the next.js federation or node.js federation plugins, already utilizing v1.5 in their canary releases.

In Rspack, Module Federation the upgraded plugin can be used through rspack.container.ModuleFederationPlugin, and the original Module Federation can be used through rspack.container.ModuleFederationPluginV1.

The Need for Redesign

The introduction of Rspack required a simpler way to maintain support across various bundler tools. To achieve this, we separated Module Federation from Webpack and implemented its runtime as a library. This move liberated federation from a significant portion of bundler lock-in.

Speed comparison to webpack federation

In a simple comparison, using a module federation example

  • Apps: 5
  • Webpack: 500–3000ms per build — production
  • Rspack: 130–350ms per build — production

Generally, we have observed 5–10x gains in build speeds of federated applications, roughly in line with typical performance gains we see with rspack. Most builds in module federation examples. Development builds we have converted typically take less than 150ms to cold start

Essential Components

The key components of this system include:

  • @module-federation/enhanced: This component provides the ModuleFederationPlugin in webpack. In Rspack, it’s implemented internally in Rust within the bundler.
  • @module-federation/runtime: Both webpack and Rspack depend on this component.
  • @module-federation/sdk: This is another component that both webpack and Rspack rely on.
  • @module-federation/webpack-bundler-runtime: This component is specific to webpack.
  • @module-federation/runtime-tools: A vanity package one can use that exports several of the module federation packages, for easier consumption.

These components might seem overwhelming, so we’ve made it easier for you. You can use @module-federation/runtime-tools as a comprehensive package. It exports several of the underlying package exports in a user-friendly way, making it easier to work with, less to worry about synchronizing upgrades of various parts.

runtime-tools can also be used in Rspack to override the default @module-federation/* packages that come with rspack. This allows users to upgrade module federation partially without needing to update the rspack core.

Introducing the Federation Runtime

Contributing to webpack plugins can be daunting due to its steep learning curve which often becomes a discouragement for community contribution. To overcome this challenge, we have shifted the federation runtime to a library. This shift reduces the need for bundler-specific knowledge, making our runtime much more user-friendly and accessible for community expansions and contributions.

The main runtime code that makes module federation what it is has been decoupled from the “bundler runtime” parts. Federation is implemented into bundlers like webpack and rspack by providing bindings for the runtime and initializing it.

Other tool chains could support Module Federation in a consistent manner if they are able to provide the bundler runtime bindings needed to initialize the runtime.

“FederationHost” Instances

At the heart of its redesign sits a class called FederationHost

What is a FederationHost?

Each container/runtime is an instance of FederationHost. The host application, as we know them, is usually the first FederationHost in the instances array. FederationHost is the class that provides additional capabilities (like runtime plugins, hooks, lifecycles, and utilities). We have added a global object that allows you access to all instances: globalThis.__FEDERATION__. If you log this (be it client or server), you should see something like:

This data and more can be accessed via our runtimePlugins APIs as well. In the future, we will expand on the data one has access to.

Note: this information is available during runtime, it is dynamic and a reflection of instantiated application.

Runtime Plugins

This is one of the most exciting additions to this initial update. Runtime plugins invert control. At ByteDance, we have many complex requirements and needs for Module Federation. Nearly all of these fell far beyond the scope of what the original plugin was designed to accomplish.

Runtime Plugins were designed to allow us and the community to extend module federation. It introduces the concept of hooks and a lifecycle, something sorely missed in the original plugin design.

Here are the hooks we currently have:

  • beforeInit
  • Init
  • beforeRequest
  • afterResolve
  • beforeInitContainer
  • initContainer
  • onLoad
  • handlePreloadModule
  • errorLoadRemote
  • beforeLoadShare
  • loadShare
  • resolveShare
  • beforePreloadRemote
  • generatePreloadAssets
  • afterPreloadRemote

You can see a practical example of combining Federation Runtime Hooks with React, to create in-app control panels.

You can read more in-depth about these hooks here.

You can find more examples:
https://github.com/module-federation/module-federation-examples/tree/master/runtime-plugins

https://github.com/module-federation/universe/tree/main/apps/runtime-demo

And general ReadMe’s:
@module-federation/enhanced
@module-federation/runtime

Real World Example of a Runtime Plugin

This specific one is currently what is used in nextjs-mf, to support Next.js
Next has been the least compatible framework to date. So it provides a very advanced framework integration example.

import { FederationRuntimePlugin } from '@module-federation/runtime/types';
export default function (): FederationRuntimePlugin {
return {
name: 'next-internal-plugin',
// if remote is offline
errorLoadRemote({ id, error, from, origin }) {
// create a fake page module for next
const pg = function () {
console.error(id, 'offline', error);
return null;
};
// add get initial props
pg.getInitialProps = function (ctx: any) {
return {};
};
let mod;
// check if this hook is being called at runtime or during build time (like in compiler macros)
if (from === 'build') {
mod = () => ({
__esModule: true,
default: pg,
getServerSideProps: () => ({ props: {} }),
});
} else {
mod = {
default: pg,
getServerSideProps: () => ({ props: {} }),
};
}
// return a fallback module which will avoid app crash on missing container
return mod;
},
// before any initialization phases start
beforeInit(args) {
if (
// ill check who is calling this hook (workaround for next)
typeof __webpack_runtime_id__ === 'string' &&
!__webpack_runtime_id__.startsWith('webpack')
) {
return args;
}
// origin allows me to access "this" FederationHost instance
const { moduleCache, name } = args.origin;
// pull embedded interface off global scope.
const gs = (globalThis as any) || new Function('return globalThis')();
const attachedRemote = gs[name];
if (attachedRemote) {
// I add a remote to the module cache manually, in this case next hosts embed their container inside the main runtime
// So i would not need to load "home" remote if the host is "home",
// I can attach it to the module cache and federation will not attempt to load remoteEntry.js
moduleCache.set(name, attachedRemote);
}
return args;
},
// called as initialization happens
init(args) {
return args;
},
// before a request is made to a container.
// you could modify the request here
beforeRequest(args) {
console.log(args.id) //==> home/carousel
// for example, im redirecting the import to "differentRemote" and importing "differentExposed"
//args.id = 'differentRemote/differentExposed'
return args;
},
// can return your own HTML script element instead of the default one.
// If you wanted to add SRI or other attributes to the script tag, you could do it here
createScript({ url }) {
// document.createElement('script);
//..etc
return;
},
// The originally requested container exists and exposed modules can be loaded (shouldnt fail if requested)
afterResolve(args) {
return args;
},
// after import/carousel has loaded, here you could modify or wrap the module.exports before its sent back to consumer
onLoad(args) {
// args.lib = wrapExports(args.lib)
return args;
},
//Control what shared modules are loaded, and from where.
resolveShare(args) {
// next.js needs react and next/* to be loaded from the host
// resolveShare allows me to tell everyone they should prefer the hosts versions regardless of what share scope chose.
if (
args.pkgName !== 'react' &&
args.pkgName !== 'react-dom' &&
!args.pkgName.startsWith('next/')
) {
return args;
}
const { shareScopeMap, scope, pkgName, version, GlobalFederation } = args;
// Find the host instance dynamically, the host is always the first instance in the array
const host = GlobalFederation['__INSTANCES__'][0];
if (!host) {
// if no host is found, return the original args
return args;
}
// replace the resolver with a function that returns the host version of the module
args.resolver = function () {
// update localShareScopeMap (overwrite previous value)
// as you can see, i am able to access any FederationHost instances original shared modules it can provide, and i can use these directly
shareScopeMap[scope][pkgName][version] = host.options.shared[pkgName]; // replace local share scope manually with desired module
// return the resolved share back to consumer
return shareScopeMap[scope][pkgName][version];
};
return args;
},
// called before loading shared modules, good place to setup rules ahead of module negotiation
async beforeLoadShare(args) {
return args;
},
};
}

Community plugins are welcome, and we will provide handy runtimePlugins for common use cases over time.

For example,

  • a retryPlugin — if remote fails to load, you could pass simple retry function or fallback
  • a fallbackPlugin — offline remote support thats easy to do
  • sharing presets, preferences, governance
  • wrap modules in error boundaries on the way in
  • personalize the codebase, override configs, create chrome debug tools
const rspack = require('@rspack/core');

new rspack.container.ModuleFederationPlugin({
name: 'app1',
filename: 'static/js/remoteEntry.js',
exposes: {
'./Button': './src/components/button.js',
},
runtimePlugins: [require.resolve('./my-custom-plugin')]
remotes: {
app2: 'app2@http://localhost:3002/static/js/remoteEntry.js',
},
shared: {
react: { singleton: true },
'react-dom': { singleton: true },
},
})

Future-Proofing our Runtime

Our runtime can be implemented and other tools only need to create the right bindings, making runtime interfaces consistent for everyone. This reduces friction and fragmentation around federation. Bundler teams could support the federation by providing “bundler runtime” packages or bindings.

The Federation plugin uses a significant amount of graph data to create its containers. Using JS hooks to expose this data to a Javascript plugin would create performance problems. It was best implemented in Rust, which meant that we would require a more robust user API to customize how it works, as native code must be compiled into rspacks core.

During the redesign, we focused on:

  1. Giving users more control over the “black box” of module federation
  2. Offering a sane, user-friendly lifecycle
  3. Providing community extension options, allowing “third-party” plugin development
  4. Reducing forking the original architecture, ensuring MFP has enough hooks and is designed in a way that doesn’t require core class changes for extension.
  5. Other Ecosystems, designing the runtime to accommodate future build ecosystems.

Future Plans

  • HMR and Fast Refresh: We have had this luxury internally, and intend to release this much-requested DX improvement to public users too.
  • Chrome Dev Tools: We will be releasing our chrome dev tools, allowing you to see and interact with the application in a new dimension
  • TypeScript Remote Types: Another regularly requested feature is TypeScript support. While there are a few attempts at this already on NPM, the Infra Team at Bytedance has the most integrated and seamless implementation to date. We look forward to releasing built-in support in the near future.
  • Improvements to Node Federation: Federation should soon provide built-in support for server-side/node, without the need for an additional compiler plugin.
  • React Server Components: In close collaboration with members of the remix team, we have proof of concepts in a functional state with React Server Components and Module Federation. We are very encouraged by preliminary outcomes and will watch the progress closely. Proof of Concept

Thank you

To the Rspack Team, and ByteDance Infra Team.
To the community
To the users

--

--

Zack Jackson

Infra Architect @ ByteDance. Creator of Module Federation. Specializing in Bundler and Javascript Orchestration at scale.