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.

Further reading:


Related posts

Adopting a micro-frontend architecture

Published

Supporting scaleable web application development with micro-frontends

Mock server-sent events (SSE) with msw

Published

Mock Service Worker supports mocking SSE

Platform team challenges

Published

Challenges faced when introducing a platform team


Thanks for reading

I'm Alex O'Callaghan and this is my personal website where I write about software development and do my best to learn in public. I currently work at Mintel as a Principal Engineer working primarily with React, TypeScript & Python.

I've been leading one of our platform teams, first as an Engineering Manager and now as a Principal Engineer, maintaining a collection of shared libraries, services and a micro-frontend architecture.