How Big Tech Builds Micro Frontends

I wrote this article myself. AI was used to rephrase and grammar check the language. I then reviewed the result and simplified it again.

  • Module Federation provides better performance, but no strong runtime isolation.
  • Iframes provide strong isolation, making independent deployment more reliable.
  • This matters especially for large, legacy codebases where enforcing boundaries in code is difficult.
  • A typed communication SDK, routing, and smart chunk splitting can reduce the performance cost.
  • For large legacy applications, iframes can be a better tradeoff than MF

If you are familiar with Micro Frontends, chances are very high you are also familiar with Module Federation. Both of these approaches have the same core idea at heart: loading a remote module at runtime into the same JS context and stitching a component from the remote module into one shared DOM object. It is a very performant approach with clear benefits. Turns out the very big players often use something very different, often looked down upon. Whenever I say this word next to developers I am surrounded by disgusted faces. I am talking about lame old iframes.

You did not misread that - the big players often prefer iframes over federation approaches, especially for large products. Google Cloud Console uses iframes for micro frontends. Microsoft uses iframes for micro frontends in many large products. Spotify, Salesforce, SAP, … - all of them use iframe-based micro frontend solutions. The question is: Why do these companies not prefer MF when that’s so much leaner on the end user performance?

Let’s dive into the rationale, why iframes are such a great fit for large scale micro frontends, the benefits, tradeoffs and what an iframe-based micro frontend framework could look like.

iframes 101

An iframe is simply an HTML element to embed external web apps. Typically used to embed elements like YouTube videos, Google Maps, Ads or other externals. So their usage is really common and the usage of an iframe is extremely simple:

html
<iframe
  title="Wikipedia page for Avocados"
  src="https://en.wikipedia.org/wiki/Avocado"></iframe>

Iframes encapsulate the rendered source inside the iframe by creating a new DOM object entirely in the iframe, such that no styles are shared, the window object is not shared, nor the DOM. This is isolation on the web in its highest form - there is no stronger isolation for embedding external sources.

iframe-based Micro Frontends

Instead of loading an external source like a YouTube video in your iframe, you could also simply load one of your frontends and embed it. For example, you could have the product catalog page on a large e-commerce platform as a standalone application, which is loaded into an iframe hosted in the shell application. While it is extremely straightforward to implement this, this architecture has some serious tradeoffs that could regress the end user experience substantially depending on your application. Because of the aforementioned isolation guarantees of iframes, you are downloading an entire application again and bootstrapping it again. That means, you are paying both additional heavy network latency cost and additional runtime cost because bootstrapping frameworks, such as Angular can be expensive on its own. Additionally, depending on your application, bootstrapping could also trigger additional cascading http requests putting additional throttle on the network. Therefore, it is a fair assessment to say iframes are inherently not a performant foundation for micro frontends. This is the major tradeoff, especially in contrast to Module Federation.

In MF, you would load a micro frontend from a remote source and would share common dependencies, such as @angular/core, rxjs, react-dom, or other core dependencies, such that you have less network throttle with these architectures. Additionally you are also saving the overhead of bootstrapping the micro frontend, because in MF the micro frontend is embedded seamlessly inside the shell’s DOM and shares the same JS execution context as the shell. It is apparent that MF are much faster - but why are some big products still not using them then?

If you have read between the lines, you already spotted that it all comes down to isolation. While iframes guarantee strong runtime isolation and pay the performance cost, MF cannot guarantee strong runtime isolation but buy themselves performance optimization. MF has no runtime isolation guarantees at all. Styles are shared by default, meaning the shell styles could affect the micro frontend styles; or even worse, your micro frontend styles could leak and affect other micro frontends. This can be solved by wrapping the micro frontend in a web component and using shadow DOM, and while that makes the isolation stronger for styles, it is still not a strong runtime isolation guarantee because the JS execution context is still shared.

PerformanceIsolation
iframesSlowStrong
Module FederationFastNone
Module Federation + Web ComponentsFastWeak

Why Isolation Matters to Big Tech

The problem gets particularly interesting when you look at the kind of codebases Big Tech companies are actually dealing with.

In a greenfield polyrepo setup, you can enforce fairly strong boundaries. If every micro frontend is owned by a separate team, has its own repository, avoids global mutations and side effects, never reaches into another application’s state, and communicates through explicit contracts, MF can probably work very well.

The problem is that large products rarely look like this. Some of these codebases have been running for more than a decade. They started long before anyone was thinking about micro frontends, and they have gone through multiple framework migrations, organizational changes and architectural rewrites. There is AngularJS code, old TypeScript patterns, shared utilities, global state, and plenty of code that was written under the assumption that it was part of one big application.

And this is where things like the window object become particularly problematic. If two micro frontends share the same JavaScript context, they also share the same global environment. One application can mutate something on window, register a global event listener, modify a prototype, introduce a global side effect or make assumptions about state created by another application. You can try to put rules in place to prevent this, with conventions, lint rules, ownership boundaries and communication contracts. But adding module boundaries as an afterthought is most of the time impossible because of hundreds and thousands of violations, that would take years to refactor and those migrations have significant risk of breaking production. Therefore it is very unlikely any of those codebases is in a state that could enforce the isolation on a code level.

More importantly, without isolation, the micro frontends become coupled at runtime. If deploying micro frontend A can break micro frontend B because they share the same execution context, then A and B are not truly independently deployable anymore. You might have separate repositories, separate teams and separate build pipelines, but you still have a runtime dependency between them. And this undermines one of the core premises of micro frontends: independent development and independent deployment. The whole point is that a team should be able to change and deploy its micro frontend without having to worry about accidentally breaking another team’s micro frontend. If that guarantee only exists as long as every team perfectly follows a set of architectural conventions, you don’t really have isolation. You have a convention that everyone has to follow.

This is why runtime isolation is a safety mechanism rather than a matter of architectural taste, and it becomes more valuable the more teams, applications and lines of code you have. The iframe does not care whether the application inside it is perfectly designed. It gives you a boundary around the application regardless of what happens inside it. A window mutation stays inside that window. A global event listener stays inside that browsing context. A CSS rule cannot accidentally leak into the host application. The isolation is no longer something you have to maintain through engineering discipline alone, because the browser enforces it at runtime. That is the reason the iframe approach starts making more sense at Big Tech scale, or more generally speaking for significantly large legacy applications.

Alternative Approaches with Isolation Guarantees

iframes are the most common and battle tested approach for strong runtime isolation. But every project is different and could try different approaches. Let’s highlight a few others that might suit your project better:

Module Federation + Web Components + Polyrepo + Message Queues

For a greenfield project, or a project in a very clean state, you could opt for isolation by enforcing strict boundaries by splitting micro frontends into multiple repos, such that you do not have to deal with module boundaries and the possible violations that could slip in.

At runtime, you could use web components to isolate the styles in a shadow DOM, but you are still at risk of global state mutations as the micro frontends in this approach still share the same execution context.

This is a weak isolation contract, because you could still break the isolation with side effects or global mutations, so it would be wise to set up a contracts project with a versioned SDK which acts as a message queue between micro frontends and the shell to reduce the degree of coupling.

While I would opt to use such an approach for a greenfield project, I would caution against it for brownfield projects, because it requires so much care and could quickly backfire and create horrible production incidents or break the independent deployability promise.

Web Fragments

This is a very new JS-framework-agnostic micro frontend framework which uses iframes in a novel way to bootstrap microfrontends in a sandboxed context and patches the components into the shell’s DOM and uses shadow DOM for style isolation. The approach is interesting and novel, but I have not used it yet, so I can’t recommend it at this point.

However, I think this is worth considering and spiking on for newer micro frontend projects.

https://web-fragments.dev/

Implementing a Micro Frontend Framework

The iframe gives us the isolation we want, but it also means we need to solve some of the problems that MF normally solve for us. The applications need to communicate, routing needs to work across application boundaries, and we need to make sure we are not paying the full performance cost of bootstrapping completely independent applications.

None of these problems are fundamentally unsolvable. In fact, this is where a dedicated micro frontend framework can provide the missing pieces.

Message Queue

The first problem is communication.

The shell and the micro frontends will need to communicate with each other. The shell might need to tell a micro frontend that the user changed some global context, while a micro frontend might need to request navigation or notify the shell that something happened.

The obvious primitive for communicating between isolated browsing contexts is window.postMessage. But exposing postMessage directly to application developers would be a mistake. We would lose type safety, make contracts implicit and leave every application responsible for implementing the communication protocol correctly.

Instead, the framework could provide an SDK with versioned contracts for all communication between the host and its micro frontends.

The SDK would have two sides: a host service running in the shell and a client service running inside each micro frontend. Both sides can emit and listen to events, but the events themselves are defined by the contracts provided by the SDK.

For example, the SDK could define:

tsx
interface NavigationRequested {
  type: 'navigationRequested';
  version: 1;
  payload: {
    url: string;
  };
}

interface UserChanged {
  type: 'userChanged';
  version: 1;
  payload: {
    id: string;
  };
}

interface ProductSelected {
  type: 'productSelected';
  version: 1;
  payload: {
    productId: string;
  };
}

The client would then get a fully typed API:

tsx
client.on('navigationRequested', event => {
  event.payload.url;
});

client.emit('productSelected', {
  productId: '123'
});

And the host would use the same contracts:

tsx
host.on('productSelected', event => {
  event.payload.productId;
});

host.emit('userChanged', {
  id: '123'
});

The SDK translates these typed events into messages sent through postMessage. The application developers never need to access the iframe’s window, inspect raw messages or manually serialize and deserialize payloads.

This also gives us an explicit compatibility boundary between the host and the micro frontend. Contracts can be versioned and evolved independently, allowing a newer micro frontend to communicate with an older host, or vice versa, as long as the relevant contract versions remain compatible.

The framework can additionally take care of concerns such as validating message origins, routing messages to the correct iframe, request/response communication, timeouts and subscription cleanup.

The important part is that we are not trying to recreate shared state across the iframe boundary.

We deliberately keep the applications isolated and provide a narrow, explicit communication channel between them.

Routing

Routing is another problem that needs to be solved at the framework level.

From the user’s perspective, the application should still behave like one application. The browser URL should represent the overall application state, even if different parts of that application are implemented by completely independent micro frontends.

The host can therefore own the top-level routing and map URL segments to micro frontends:

/products/*       → Product Micro Frontend
/settings/*       → Settings Micro Frontend
/analytics/*      → Analytics Micro Frontend

When the route changes, the host can load or activate the corresponding iframe. The micro frontend can then use its own internal router for navigation within its assigned route.

The same SDK can be used for navigation events. A micro frontend does not modify the host’s router directly. It emits a typed navigation event, and the host decides how that request should be handled.

This keeps routing ownership outside of the micro frontend while still allowing the micro frontend to participate in application-wide navigation.

Chunk Splitting

The biggest downside of the iframe approach is still performance. Every micro frontend is a separate application with its own JavaScript context, so there is no runtime module sharing to fall back on. What we can control is how much each application has to download before it becomes interactive, and how much of that it has to download again the next time the user comes back.

The first lever is the initial bootstrap. A micro frontend should not ship its entire feature set just to render its landing route. Modern bundlers such as Rolldown or Webpack can split an application into an entry chunk, a vendor chunk and lazily loaded route chunks, so that opening the product catalog does not download the checkout flow, the admin screens and every icon set the application owns. Barrel files are worth watching here, because a single re-export can drag a whole library into the entry chunk and quietly undo the split.

Products
├── products.4f2a91.js     entry
├── vendor.9c17be.js       framework and shared libraries
├── catalog.b83d05.js      lazy route
└── checkout.1d94af.js     lazy route

The second lever is the return visit. Splitting vendor code away from application code is not only about the size of the first download, it is also about how often that download has to be repeated. Framework code changes rarely, product code changes daily. If both live in the same chunk, every deployment invalidates the framework bytes as well. Kept apart, with content-hashed filenames and long-lived immutable cache headers, a deployment only invalidates the chunks that actually changed and the browser serves the rest from its own cache.

This does not turn iframes into Module Federation. Browsers partition their HTTP cache, so this is about the same user returning to the same product, not about one micro frontend borrowing bytes another one has already downloaded. Each iframe still downloads and evaluates its own copy within its own JavaScript context. But for a large product where users come back every day, careful chunk splitting and cache headers take a meaningful part of the cost out of the architecture.

Conclusion

Micro frontends are not really about splitting a frontend into smaller pieces. They are about creating boundaries between independently owned parts of a product.

Module Federation and Native Federation are excellent at making those boundaries cheap. They allow us to load code dynamically, share dependencies and integrate applications almost as if they were one application. But they do so by giving up runtime isolation.

For a greenfield application with well-defined boundaries, strong engineering practices and little legacy code, that tradeoff can be completely reasonable.

For a large, long-lived product, the tradeoff can look very different.

When you have millions of lines of code, hundreds of teams and years of accumulated technical debt, enforcing isolation through code and engineering conventions becomes increasingly difficult. And without runtime isolation, independent deployment eventually becomes a much weaker guarantee.

This is where iframes become interesting.

They are slower. They require bootstrapping another application. They introduce additional network and runtime overhead. These are real disadvantages, and pretending otherwise does not make the architecture better.

But in exchange, the browser gives you an extremely strong runtime boundary. One micro frontend cannot accidentally mutate another micro frontend’s window, leak its styles, or introduce arbitrary JavaScript side effects into another application. Communication has to cross an explicit boundary, which means it can be defined through contracts and versioned independently.

And the performance problems are not necessarily as absolute as they first appear. A framework can build on top of iframes with typed communication contracts, routing, aggressive chunk splitting and browser caching to recover some of the performance that is otherwise lost.

Discussion