Dynamically load remoteEntry.js files
Posted 11 August 2023
Webpack Module Federation can be used to support a micro-frontend web application architecture. Independently deployed remote applications can be loaded by loading a remoteEntry.js
file. Loading these files dynamically can give you more control over performance, caching and enable features like A/B testing or branch releases.
At Mintel we use Module Federation in the context of a shell "host" application that loads separate applications maintained by independent, autonomous teams. You can read "Adopting a micro-frontend architecture" for more detail on our journey to this approach.
By default Webpack Module Federation will load the remoteEntry.js
file for every remote. Over time as teams developed new applications our host had a lot of remotes. We didn't need to load these all at once - each application works independently and users often do something on one without navigating across. Loading so many remoteEntry.js
files was having an impact on the initial load time of the application.
Using the Dynamic Remote Containers approach you can take control of loading the remoteEntry.js
files.
Remove the remotes
field from the ModuleFederationPlugin
configuration:
plugins: [
new ModuleFederationPlugin({
name: 'host-app',
remotes: {},
}),
]
Manually load the remoteEntry.js
in your own code by injecting a <script>
tag:
const remoteEntryUrl = "https://something/remote-app/remoteEntry.js";
const scope = "remote-app";
const moduleName = "App";
await __webpack_init_sharing__("default");
await new Promise<void>((resolve, reject) => {
const element = document.createElement("script");
element.src = remoteEntryUrl;
element.type = "text/javascript";
element.async = true;
element.onload = () => {
element.parentElement?.removeChild(element);
resolve();
};
element.onerror = err => {
element.parentElement?.removeChild(element);
reject(err);
};
document.head.appendChild(element);
});
// Initialize the federated module
const container: any = window[scope as any];
await container.init(__webpack_share_scopes__.default);
// Fetch module exposed by the federated module
const factory = await container.get(moduleName);
return factory();
The module-federation-import-remote package can provide this logic for you:
import { importRemote } from "module-federation-import-remote";
importRemote({ url: "https://something/remote-app", scope: "remote-app", module: "App" }).then((App) => {...});
// If App is a React component you can use it with lazy and Suspense just like a dynamic import:
const App = lazy(() => importRemote({ url: "https://something/remote-app", scope: "remote-app", module: "App" }));
return (
<Suspense fallback={<div>Loading App...</div>}>
<App />
</Suspense>
);
Using this approach we were able to improve initial load performance by delaying loading the remoteEntry.js
until a React component actually needed to be rendered. This control over how you load remotes also can allow you to dynamically load different remoteEntry.js
files at runtime, perhaps based on a feature flag or user properties.
importRemote
also adds a cache-busting query param to the remoteEntry.js
url by default to help ensure these entrypoints aren't cached. Be aware of the limitations of query string cache busting, as discussed in this GitHub thread - you should probably also think about configuration of your CDN or introducing a service to help manage versioning of remotes.