Quizzr Logo

Micro-Frontends

Optimizing Performance by Managing Shared Dependencies and Bundle Bloat

Master dependency sharing in Module Federation to prevent loading multiple versions of React or other large libraries across your modules.

ArchitectureAdvanced12 min read

The Architecture of Shared Dependencies

In a traditional monolithic application, dependency management is a centralized task handled by a single package file. Every component and utility within the monolith relies on the same shared set of libraries installed in the root directory. This ensures that the application only loads a single instance of large packages like React, styled-components, or Lodash into the browser memory.

When teams transition to a micro-frontend architecture, this centralized control often vanishes. Each micro-frontend is built and deployed as an independent application with its own build pipeline and configuration. Without a strategic approach to dependency sharing, the browser could end up loading three different versions of the same framework simultaneously.

Loading multiple versions of a large library creates a massive performance penalty for the end user. It increases the initial download size, extends the script parsing time, and consumes excessive amounts of client-side memory. Beyond performance, certain libraries are designed to function as singletons and will break if multiple instances attempt to manage the same global state or document object model.

Redundant library loading is the single greatest threat to the scalability of micro-frontend architectures. A failure to share core dependencies effectively turns a modular system into a series of competing monoliths that degrade the user experience.

Module Federation solves this by introducing a negotiation layer during the application runtime. It allows different independently built modules to check for existing instances of a library before deciding to load their own copy. This process transforms how we think about bundling, moving from a static inclusion model to a dynamic runtime agreement.

The Dependency Tax in Distributed Systems

Every shared library included in a micro-frontend bundle represents a hidden cost known as the dependency tax. If four different teams each bundle a 40 kilobyte library, the user pays that price four times over for the same functionality. This redundancy quickly scales until the total payload exceeds the limits of mobile devices and slow network connections.

The technical challenge lies in balancing team autonomy with collective efficiency. Teams need the freedom to upgrade their dependencies at their own pace without breaking the entire application shell. Dependency sharing in Module Federation provides the infrastructure to allow this flexibility while maintaining a thin, optimized runtime footprint.

Version Mismatches and Runtime Risks

Version mismatches occur when different micro-frontends require conflicting versions of the same dependency. For example, a legacy dashboard might require React 16 while a new search module requires React 18. These conflicts can lead to cryptic errors and inconsistent behavior across the user interface.

Module Federation identifies these conflicts during the module loading phase rather than at build time. It uses a manifest to compare the versions requested by each remote and the host application. This allows developers to define strict rules about which versions are compatible and which should trigger a fallback mechanism.

Orchestrating Global Singletons

To implement dependency sharing, developers use the Module Federation Plugin configuration within their build tool. The most critical part of this configuration is the shared property, which acts as a registry for common libraries. This registry tells the bundler which packages should be treated as external references rather than static imports.

The shared configuration supports several options that control how the runtime handles library instances. The singleton property is particularly important for libraries that manage application-wide state or use global event listeners. When a package is marked as a singleton, the system guarantees that only one version of that package will be instantiated throughout the entire session.

javascriptBasic Shared Configuration
1const { ModuleFederationPlugin } = require('webpack').container;
2
3module.exports = {
4  plugins: [
5    new ModuleFederationPlugin({
6      name: 'order_service',
7      filename: 'remoteEntry.js',
8      remotes: {
9        shell: 'shell@http://localhost:3000/remoteEntry.js',
10      },
11      // Defining which dependencies to share with the host and other remotes
12      shared: {
13        react: {
14          singleton: true, // Only one instance of React should exist
15          requiredVersion: '^18.2.0',
16          eager: false // Load react only when it is actually needed
17        },
18        'react-dom': {
19          singleton: true,
20          requiredVersion: '^18.2.0'
21        }
22      }
23    })
24  ]
25};

By setting the singleton flag to true, you inform the federation runtime that multiple versions of this library cannot coexist safely. If the system encounters two different versions of a singleton, it will attempt to pick the highest compatible version. This prevents the common issue of having multiple React contexts or event systems fighting for control over the browser.

Required Versions and Strict Versioning

The requiredVersion property allows you to specify a semantic version range that your module can support. This follows the standard npm versioning logic, using carats or tildes to define compatibility boundaries. If a remote needs a version outside of this range, the system must decide how to proceed based on your configuration.

If you enable the strictVersion property, the application will throw a runtime error if it cannot find a version that satisfies the requirements. This is a defensive programming technique used to prevent difficult-to-debug UI bugs caused by incompatible library versions. In production environments, this ensures that your micro-frontend never runs on a foundation it was not tested against.

The Role of the Host Application

The host application, often called the shell, usually takes the lead in managing the shared scope. It is responsible for providing the primary versions of core libraries to its child remotes. When the shell initializes, it populates the shared scope with its own versions, which remotes can then consume without downloading their own copies.

Remotes are designed to be resilient and can still function if they are loaded in isolation. If a remote is accessed directly and no shared scope is provided by a host, it will fall back to its own bundled version of the library. This dual-mode capability is essential for independent testing and local development workflows.

Solving Conflict Strategies and Trade-offs

Handling conflicts in a distributed system requires a clear strategy for version negotiation. When two modules request different versions of a non-singleton library, Module Federation can actually load both versions if they are functionally compatible. This approach avoids breaking the application but increases the total bundle size as a trade-off.

Managing these trade-offs involves making decisions about which libraries are core to the platform and which are module-specific. General utility libraries like Lodash are good candidates for sharing because multiple versions can often coexist without side effects. Frameworks and design system libraries require much stricter control to ensure visual and functional consistency.

  • Singleton Strategy: Best for libraries with global side effects like React or Redux where multiple instances break the app.
  • Version Range Strategy: Allows the highest compatible version to be used across all modules to reduce total downloads.
  • Strict Versioning: Prevents execution if the available dependency does not perfectly match the requirements of the module.
  • Eager Loading: Forces the dependency to be loaded upfront, which is useful for the host but can bloat the entry point.

It is often beneficial to automate the shared configuration by pulling dependency versions directly from the package.json file. This reduces manual maintenance and ensures that the Module Federation configuration stays in sync with the actual versions used during development and testing. This technique is especially useful in monorepos where many modules share the same root dependencies.

Automating Dependency Management

Manually updating version strings in a plugin configuration is prone to human error. Instead, developers can use the spread operator to import the dependencies object from their package file. This allows the build system to dynamically generate the shared configuration based on the current environment.

javascriptDynamic Versioning with Package JSON
1const deps = require('./package.json').dependencies;
2
3module.exports = {
4  plugins: [
5    new ModuleFederationPlugin({
6      // Automatically sharing all dependencies listed in package.json
7      shared: {
8        ...deps,
9        react: {
10          singleton: true,
11          requiredVersion: deps.react,
12        },
13        'react-dom': {
14          singleton: true,
15          requiredVersion: deps['react-dom'],
16        },
17      },
18    })
19  ]
20};

The Impact of Eager Loading

By default, shared dependencies are loaded asynchronously to prevent blocking the initial page render. This is usually the desired behavior, but it requires the application to have an asynchronous entry point. If the entry point is synchronous, the application will fail because the shared libraries are not yet available.

The eager property allows you to bypass this asynchronous behavior for specific modules. Setting eager to true is common for the host application because it needs those libraries immediately to render the shell. However, overuse of the eager flag in remotes defeats the purpose of code splitting and should be handled with extreme caution.

Optimization and Real-World Implementation

In a real-world enterprise environment, managing shared dependencies requires a governance model. Teams should agree on a set of core dependencies that are standardized across the organization. This might include the UI library, state management tools, and internationalization utilities that every micro-frontend is expected to use.

A common pitfall is sharing too many libraries, which creates tight coupling between independent teams. If every tiny utility library is shared, a change in one team's package version might force every other team to update their configurations. The goal is to share only the large, high-impact libraries that significantly affect performance or functional integrity.

Monitoring the shared scope is essential for long-term maintenance of a micro-frontend ecosystem. Developers should use bundle analysis tools to verify that only one instance of a library is being shipped to the browser. If the same package appears multiple times in the network trace, it indicates a misconfiguration in the singleton or version negotiation logic.

Successfully mastering dependency sharing results in a system that feels like a monolith to the user but acts as a fleet of microservices for the developer. This balance allows for rapid iteration and deployment while maintaining the high performance standards expected of modern web applications.

Designing a Shared UI Library

Shared UI libraries present a unique challenge because they often contain both components and styles. If different remotes use different versions of a design system, the user interface will look inconsistent and fragmented. These libraries should almost always be treated as singletons within the federation configuration.

When sharing a UI library, it is important to ensure that the CSS is also handled correctly. If the library uses global styles, loading multiple versions will cause style collisions that are difficult to resolve. Using CSS-in-JS or CSS Modules can help mitigate these issues, but version alignment remains the most reliable solution.

The Importance of Async Entry Points

To make Module Federation work correctly without the eager flag, you should use a bootstrap pattern. This involves creating a secondary entry file that imports the main application logic. The primary entry point simply uses a dynamic import to load this bootstrap file, allowing the federation runtime to resolve shared modules first.

This pattern provides a critical window of time for the runtime to negotiate versions and fetch the required chunks. Without this asynchronous bridge, the application would attempt to execute code before its dependencies are ready. Implementing the bootstrap pattern is considered a best practice for all micro-frontend architectures using Module Federation.

We use cookies

Necessary cookies keep the site working. Analytics and ads help us improve and fund Quizzr. You can manage your preferences.