The Sanctuary

Writing about interests; Computer Science, Philosophy, Artificial Intelligence and more!

Micro-Frontends: From Monolith to Module Federation

JavaScript started quite timidly, modest and small. Its main purpose was adding interactivity to web pages. Back in the days, other technologies aimed for the same end: Java applets, VBScript and ActiveX.

In the Web 1.0 era, JavaScript relied on the engine designed by Brendan Eich in 1995 for the Netscape Browser, which evolved into SpiderMonkey—Firefox’s JavaScript engine still used today. But there was another ambitious project called V8, released by Google in 2008 as part of the Chromium project.

The V8 innovation led directly to the exaltation of JavaScript to the server side, allowing JavaScript execution outside a browser, with fully-fledged frameworks such as Express.js and more recent Next.js.

A Bit of History

As the Spanish philosopher George Santayana said: “Those who don’t know history are doomed to repeat it.” In order to grasp the current state of the art, we need to delve into its roots.

Web 1.0 refers to the original, information-oriented version of the World Wide Web. Created by Tim Berners-Lee in 1989/1990, it consisted of largely static webpages developed by a small number of authors for consumption by a large audience. — Michael Thomas, Handbook of Research on Web 2.0 and Second Language Learning (2008)

The following diagram illustrates the high-level functioning of the Web in its infancy:

Web 1.0 Architecture

The Web 1.0 paradigm couldn’t constitute a sustainable model. Users needed more—and the ability to share content. Web 2.0 emerged as the social web, where technologies from blogs and wikis through social networking sites to podcasting and virtual worlds became about communicative networking.

The barrier between classical heavy client applications and web applications shattered over time.

State of the Art: The Rise of SPAs

The shrinking barriers between heavy clients and web apps gave rise to a new hybrid approach, in which interaction with the user is performed by dynamically rewriting the application state with data from the server. This new paradigm signed the emancipation of Single Page Applications (SPAs).

Single Page Application Architecture

“Better late than never” was the motto, giving rise to the frontend monolith with all of its “glory”, mimicking the backend modular model: modules, components, services, data binding, routing. A lot of frameworks loomed out of the dark age of spaghetti scripts: AngularJS, Ember.js, Knockout.js, Meteor.

The Problems with SPAs

Performance

Transposing the heavy client to the web comes at a cost: the initial loading time of all the JavaScript takes significant time, in conjunction with the processing cost of running both the browser and the JS engine threads. The extensive memory consumption and leaks compound the problem.

This might not be true for small to medium-sized apps, but it strongly holds for bigger, complex applications.

Complexity and Maintainability

As the holy SPA monolith grows in size, so does complexity and the cost of maintainability. The SPA’s “One ring to rule them all” maxim can only hold to a certain extent, for evolution is the essence of existence.

While a monolith still usually works out to be an efficient and productive solution—and many projects would benefit from the traditional paradigm—a modern approach lurks beyond its climax.

The Micro (R)evolution

After the frontend’s war for independence from backend hegemony and the establishment of the SPA’s republic, it was time to think outside the box.

The “Need for Speed” and efficiency requires further separation of concerns—this time at a higher level than conventional modularity and SOLID principles.

The success of the microservices architectural pattern heavily influenced frontend trends. What if individual segments of a web application could be developed and deployed independently, without compromising overall system availability?

Instead of building the frontend from a single codebase using a single framework, decomposing the application implies that each component is a standalone application with its own dependencies, build cycle, and deployment pipeline.

Implementation Approaches

iFrames

The simplest approach uses iFrames with message passing for communication:

<!DOCTYPE html>
<html>
<head>
    <title>iFrames MFE</title>
</head>
<body>
    <iframe src="./dashboard/index.html" id="dashboard"></iframe>
    <iframe src="./orders/index.html" id="orders"></iframe>
    <script src="app.js"></script>
</body>
</html>
// app.js - Cross-iframe communication
window.addEventListener(
  "message",
  (event) => {
    document
      .querySelectorAll("iframe")
      .forEach(iframe => iframe.contentWindow.postMessage(event.data, "*"));
  },
  false
);

Webpack Module Federation

Module Federation allows loading remote modules at runtime:

// app-shell/webpack.config.js
const ModuleFederationPlugin = require("webpack/lib/container/ModuleFederationPlugin");

module.exports = {
  plugins: [
    new ModuleFederationPlugin({
      name: "shell",
      filename: "remoteEntry.js",
      remotes: {
        Nav: "Navigation@http://localhost:3001/remoteEntry.js",
        Sidebar: "Sidebar@http://localhost:3002/remoteEntry.js",
        Dashboard: "Dashboard@http://localhost:3003/remoteEntry.js"
      },
      shared: {
        "react": { singleton: true },
        "react-dom": { singleton: true },
        "@mui/material": { singleton: true }
      }
    })
  ]
};

Podium (Server-Side Composition)

Podium uses Express.js to serve composable fragments called “podlets”:

// Podlet.js
import express from 'express';
import Podlet from '@podium/podlet';

const app = express();
const podlet = new Podlet({
    name: 'myPodlet',
    version: '1.0.0',
    pathname: '/',
});

app.use(podlet.middleware());

app.get(podlet.content(), (req, res) => {
    res.status(200).podiumSend(`
        <div>This is the podlet's HTML content</div>
    `);
});

app.listen(7100);
// Layout.js - Composing podlets
import Layout from '@podium/layout';

const layout = new Layout({ name: 'myLayout', pathname: '/' });
const podlet = layout.client.register({
    name: 'myPodlet',
    uri: 'http://localhost:7100/manifest.json',
});

app.get('/', async (req, res) => {
    const content = await podlet.fetch();
    res.send(`<html><body>${content}</body></html>`);
});

Common Concerns

Each approach addresses the same set of concerns differently:

  • Scripting scopes: How JavaScript executes in isolation
  • Assets and styles: CSS isolation (Shadow DOM, CSS-in-JS, namespacing)
  • Routing: Navigation between micro-frontends
  • Inter-MFE communication: Sharing state and events

In contrast to microservices, micro-frontends bring increased complexity:

  • Payload size (multiple framework instances)
  • Governance complexity
  • Performance and security
  • Team productivity and organization

The following graph illustrates the relationship between complexity and team productivity when using either SPAs or micro-frontends:

Complexity vs Productivity: SPAs vs Micro-Frontends

Architectural Patterns

The Multi-SPA Pattern

Think of micro-frontends as a swarm of SPAs linking to each other, with shared components and libraries. A reverse proxy simulates the behavior of a traditional monolith.

Multi-SPA Pattern

This is the simplest approach for teams embracing the paradigm—two independent SPAs respond to their respective routes while sharing navigation components and UI libraries.

The Micro-Apps Pattern

More “micro” than Multi-SPA: each component is a truly independent application developed, built, and deployed separately.

Micro-Apps Pattern

An App Shell loads first and handles loading other components, routing, and lifecycle. Central security and state management systems live within the shell.

Each micro-app runs on its own infrastructure, allowing:

  • Selective composition in response to user interaction
  • Technology independence
  • Failure isolation
  • Flexibility

The orders team can modify, build, and deploy their components without impacting the whole system. The application runs fine even if the orders component is rebooting or failing.

Composition Strategies

Client-Side Composition

Lazy-loads components from a parsed App Shell. Webpack’s Module Federation leads this approach.

Client-Side Composition

Pros: Truly independent and isolated Cons: Performance concerns when using different frameworks—response time and bundle size grow

Server-Side Composition

Restrains performance loss by composing on the server. Podium is a promising framework for this approach.

Server-Side Composition

Pros: Optimized loading time on the client Cons: Requires server-side architecture—scalability, reliability, and availability implications

Team Organization

Horizontal Teams

Each team is responsible for a single domain while contributing to shared components and libraries. Teams focus on features across the application.

Horizontal Team Organization

Vertical Teams

Each team focuses energy on a single component, enforcing domain decomposition, team independence, and technology independence. Aligns well with Domain-Driven Design principles.

Vertical Team Organization

Benefits of Micro-Frontends

When the philosophy is properly adopted and technical aspects adequately implemented:

  • Scalability: Independent scaling of components
  • Failure isolation: One component’s failure doesn’t bring down the system
  • Independent development: Separate builds and deployments
  • Promotes DDD: Automation and DevOps culture

Challenges and Cautions

Exercise utmost vigilance when dealing with micro-frontends:

  • Complexity cost: Implementing, deploying, and managing distributed frontends
  • Performance and security: XSS, CSRF vulnerabilities across boundaries
  • Code duplication: Especially in Multi-SPA patterns
  • Cross-team coordination: Communication overhead

Conclusion

Micro-frontends represent the next evolution in frontend architecture—applying the lessons learned from microservices to the browser. But unlike backend services running in isolated containers, everything in micro-frontends runs on the same browser instance, making the technical constraints more demanding.

The key insight: micro-frontends solve organizational problems as much as technical ones. They enable teams to work independently, deploy frequently, and own their domains end-to-end. But they come with complexity costs that only make sense at sufficient scale.

Choose wisely. For most projects, a well-structured monolith remains the efficient choice. Reserve micro-frontends for when the organizational benefits outweigh the technical complexity.

Conclusion


Micro-Frontends: From Monolith to Module Federation

A comprehensive study on the evolution of frontend architecture.

Achraf SOLTANI — June 15, 2024