Web front-end
12 minute read

The Strengths and Benefits of Micro Frontends

Bob Myers is a front-end architect who has consulted on large-scale projects in fields ranging from education and publishing to finance.

Micro-frontend architecture is a design approach in which a front-end app is decomposed into individual, semi-independent “microapps” working loosely together. The micro-frontend concept is vaguely inspired by, and named after, microservices.

The benefits of the micro-frontend pattern include:

  1. Micro-frontend architectures may be simpler, and thus easier to reason about and manage.
  2. Independent development teams can collaborate on a front-end app more easily.
  3. They can provide a means for migrating from an “old” app by having a “new” app running side by side with it.

Although micro frontends have been getting a lot of attention lately, as of yet there is no single dominant implementation and no clear “best” micro-frontend framework. In fact, there is a variety of approaches depending on the objectives and requirements. See the bibliography for some of the better-known implementations.

In this article, we will skip over much of the theory of micro frontends. Here’s what we won’t cover:

  • “Slicing” an app into microapps
  • Deployment issues, including how micro frontends fit into a CI/CD model
  • Testing
  • Whether microapps should be in one-to-one alignment with microservices on the backend
  • Criticisms of the micro-frontend concept
  • The difference between micro frontends and a plain old component architecture

Instead, we will present a micro-frontend tutorial focusing on a concrete implementation, highlighting the important issues in micro-frontend architecture and their possible solutions.

Our implementation is called Yumcha. The literal meaning of “yum cha” in Cantonese is “drinking tea,” but its everyday meaning is “going out for dim sum.” The idea here is that the individual microapps within a macroapp (as we shall call the composed, top-level app) are analogous to the various baskets of bite-size portions brought out at a dim sum lunch.

Overview illustration of an example micro frontend–based app as described above.

We will sometimes refer to Yumcha as a “micro-frontend framework.” In today’s world, the term “framework” is usually used to refer to Angular, React, Vue.js, or other similar superstructures for web apps. We are not talking about a framework in that sense at all. We call Yumcha a framework just for the sake of convenience: It is actually more of a set of tools and a few thin layers for building micro frontend–based apps.

Micro-frontend Tutorial First Steps: Markup for a Composed App

Let’s dive in by thinking about how we might define a macroapp and the microapps that make it up. Markup has always been at the heart of the web. Our macroapp will therefore be specified by nothing more complicated than this markup:

<html>
  <head>
    <script src="/yumcha.js"></script>
  </head>
  <body>
    <h1>Hello, micro-frontend app.</h1>

    <!-- HERE ARE THE MICROAPPS! -->
    <yumcha-portal name="microapp1" src="https://microapp1.example.com"></yumcha-portal>
    <yumcha-portal name="microapp2" src="https://microapp2.example.com"></yumcha-portal>

  </body>
</html>

Defining our macroapp using markup gives us full access to the power of HTML and CSS to lay out and manage our microapps. For example, one microapp could sit on top of another one, or to its side, or be up in the corner of the page, or be in one pane of an accordion, or remain hidden until something happens, or stay in the background permanently.

We have named the custom element used for microapps <yumcha-portal> because “portal” is a promising term for microapps used in the portal proposal, an early attempt at defining a standard HTML element for use in micro frontends.

Implementing the <yumcha-portal> Custom Element

How should we implement <yumcha-portal>? Since it’s a custom element, as a web component, of course! We can choose from among a number of strong contenders for writing and compiling micro-frontend web components; here we will use LitElement, the latest iteration of the Polymer Project. LitElement supports TypeScript-based syntactic sugar, which handles most of the custom element boilerplate for us. To make <yumcha-portal> available to our page, we have to include the relevant code as a <script>, as we did above.

But what does <yumcha-portal> actually do? A first approximation would be for it to just create an iframe with the specified source:

  render() {
    return html`<iframe src=${this.src}></iframe>`;
  }

…where render is the standard LitElement rendering hook, using its html tagged template literal. This minimal functionality might be almost enough for some trivial use cases.

Embedding Microapps in iframes

iframes are the HTML element that everyone loves to hate, but actually they provide extremely useful, rock-solid sandboxing behavior. However, there is still a long laundry list of issues to be aware of when using iframes, with potential impact on the behavior and functionality of our app:

  • First, iframes have well-known quirks in terms of how they size and lay themselves out.
  • CSS will of course be completely isolated to the iframe, for better or worse.
  • The browser’s “back” button will work reasonably well, although the current navigational status of the iframe will not be reflected in the page’s URL, so we could neither cut and paste URLs to get to the same state of the composed app, nor deep link to them.
  • Communication with the iframe from the outside, depending on our CORS setup, might need to go through the postMessage protocol.
  • Arrangements will have to be made for authentication across iframe boundaries.
  • Some screen readers may stumble at the iframe boundary or need the iframe to have a title they can announce to the user.

Some of these issues can be avoided or mitigated by not using iframes, an alternative we discuss later in the article.

On the plus side, the iframe will have its own, independent Content-Security-Policy (CSP). Also, if the microapp that the iframe points to uses a service worker or implements server-side rendering, everything will work as expected. We can also specify various sandboxing options to the iframe to limit its capabilities, such as being able to navigate to the top frame.

Some browsers have shipped or are planning to ship a loading=lazy attribute for iframes, which defers loading below-the-fold iframes until the user scrolls near them, but this doesn’t provide the fine-grained control of lazy loading that we want.

The real problem with iframes is that the content of the iframe will take multiple network requests to retrieve. The top-level index.html is received, its scripts are loaded, and its HTML is parsed—but then the browser must initiate another request for the iframe’s HTML, wait to receive it, parse and load its scripts, and render the iframe’s contents. In many cases, the iframe’s JavaScript would then still have to spin up, make its own API calls, and show meaningful data only after those API calls return and the data is processed for viewing.

This will likely result in undesirable delays and rendering artifacts, especially when several microapps are involved. If the iframe’s app implements SSR, that will help but still not avoid the necessity for additional round trips.

So one of the key challenges we face in designing our portal implementation is how to deal with this round-trip issue. Our goal is that a single network request should bring down the entire page with all of its microapps, including whatever content each of them is able to prepopulate. The solution to this problem lies in the Yumcha server.

The Yumcha Server

A key element of the micro-frontend solution presented here is to set up a dedicated server to handle microapp composition. This server proxies requests to the servers where each microapp is hosted. Granted, it will require some effort to set up and manage this server. Some micro-frontend approaches (e.g., single-spa) attempt to dispense with the need for such special server setups, in the name of ease of deployment and configuration.

However, the cost of setting up this reverse proxy is more than offset by the benefits we gain; in fact, there are important behaviors of micro frontend–based apps that we simply can’t achieve without it. There are many commercial and free alternatives to setting up such a reverse proxy.

The reverse proxy, in addition to routing microapp requests to the appropriate server, also routes macroapp requests to a macroapp server. That server dishes up the HTML for the composed app in a special way. Upon receiving a request for index.html from the browser by way of the proxy server at a URL such as http://macroapp.example.com, it retrieves the index.html and then subjects it to a simple but crucial transformation before returning it.

Specifically, the HTML is parsed for <yumcha-portal> tags, which can be done easily with one of the competent HTML parsers available in the Node.js ecosystem. Using the src attribute to <yumcha-portal>, the server running the microapp is contacted and its index.html is retrieved—including server-side rendered content, if any. The result is inserted into the HTML response as a <script> or <template> tag, so as not to be executed by the browser.

Illustration of Yumcha's server architecture. The browser communicates with the reverse proxy, which in turn communicates with the macroapp and each of the microapps. The macroapp step transforms and prepopulates the app's main index.html file.

The advantages of this setup include, first and foremost, that on the very first request for the index.html for the composed page, the server can retrieve the individual pages from the individual microapp servers in their entirety—including SSR-rendered content, if any—and deliver a single, complete page to the browser, including the content which can be used to populate the iframe with no additional server round trips (using the underused srcdoc attribute). The proxy server also ensures that any details of where the microapps are being served from are cloaked from prying eyes. Finally, it simplifies CORS issues, since application requests are all being made to the same origin.

Back at the client, the <yumcha-portal> tag gets instantiated and finds the content where it was placed in the response document by the server, and at the appropriate time renders the iframe and assigns the content to its srcdoc attribute. If we are not using iframes (see below), then the content corresponding to that <yumcha-portal> tag is inserted either into the custom element’s shadow DOM, if we are using that, or directly inline in the document.

At this point, we already have a partially functioning micro frontend–based app.

This is just the tip of the iceberg in terms of interesting functionality for the Yumcha server. For example, we would want to add features to control how HTTP error responses from the microapp servers are handled, or how to deal with microapps that respond very slowly—we don’t want to wait forever to serve the page if one microapp is not responding! These and other topics we will leave for another post.

The Yumcha macroapp index.html transformation logic could easily be implemented in a serverless lambda-function fashion, or as middleware for server frameworks such as Express or Koa.

Stub-based Microapp Control

Moving back to the client side, there is another aspect to how we implement microapps that is important for efficiency, lazy loading, and jank-free rendering. We could generate the iframe tag for each microapp, either with a src attribute—which makes another network request—or with the srcdoc attribute filled in with the content populated for us by the server. But in both those cases, the code in that iframe will kick off immediately, including loading all its script and link tags, bootstrapping, and any initial API calls and related data processing—even if the user never even accesses the microapp in question.

Our solution to this problem is to initially represent microapps on the page as tiny inactivated stubs, which can then be activated. Activation can be driven either by the microapp’s region coming into view, using the underused IntersectionObserver API, or more commonly by pre-notifications sent from the outside. Of course, we can also specify that the microapp be activated immediately.

In any case, when and only when the microapp is activated is the iframe actually rendered and its code loaded and executed. In terms of our implementation using LitElement, and assuming the activation status is represented by an activated instance variable, we would have something like:

render() {
  if (!this.activated) return html`{this.placeholder}`;
  else return html`
    <iframe srcdoc="${this.content}" @load="${this.markLoaded}"></iframe>`;
}

Inter-microapp Communication

Although the microapps making up a macroapp are by definition loosely coupled, they still need to be able to communicate with each other. For example, a navigation microapp would need to send out a notification that some other microapp just selected by the user should be activated, and the app to be activated needs to receive such notifications.

In line with our minimalist mindset, we want to avoid introducing a lot of message-passing machinery. Instead, in the spirit of web components, we will use DOM events. We provide a trivial broadcast API that pre-notifies all stubs of an impending event, waits for any which have requested to be activated for that event type to be activated, and then dispatches the event against the document, on which any microapp can listen for it. Given that all our iframes are same-origin, we can reach out from the iframe to the page and vice-versa to find elements against which to fire events.

Routing

In this day and age, we have all come to expect the URL bar in SPAs to represent the application’s view state, so we can cut, paste, mail, text, and link to it to jump directly to a page within the app. In a micro-frontend app, however, the application state is actually a combination of states, one for each microapp. How are we to represent and control this?

The solution is to encode each microapp’s state into a single composite URL and using a small macroapp router which knows how to put that composite URL together and pick it apart. Unfortunately, this requires Yumcha-specific logic in each microapp: to receive messages from the macroapp router and update the microapp’s state, and conversely to advise the macroapp router of changes in that state so the composite URL can be updated. For example, one could imagine a YumchaLocationStrategy for Angular, or a <YumchaRouter> element for React.

A composite URL representing a macroapp state. Its query string decodes to two separate (doubly encoded) query strings that are then to be passed to the microapps whose ids are specified as their keys.

The Non-iframe Case

As mentioned above, hosting microapps in iframes does have some downsides. There are two alternatives: include them directly inline in the page’s HTML, or place them in the shadow DOM. Both alternatives mirror the pros and cons of iframes somewhat, but sometimes in different ways.

For example, individual microapp CSP policies would have to be somehow merged. Assistive technologies such as screen readers should work better than with iframes, assuming they support the shadow DOM (which not all do yet). It should be straightforward to arrange to register a microapp’s service workers using the service worker concept of “scope,” although the app would have to ensure that its service worker is registered under the app’s name, not "/". None of the layout issues associated with iframe apply to the inlined or shadow DOM methods.

However, applications built using frameworks such as Angular and React are likely to be unhappy living inline or in the shadow DOM. For those, we are likely going to want to use iframes.

The inline and shadow DOM methods differ when it comes to CSS. CSS will be cleanly encapsulated in the shadow DOM. If for some reason we did want to share outside CSS with the shadow DOM, we would have to use constructable stylesheets or something similar. With inlined microapps, all CSS would be shared throughout the page.


In the end, implementing the logic for inline and shadow DOM microapps in <yumcha-portal> is straightforward. We retrieve the content for a given microapp from where it was inserted into the page by the server logic as an HTML <template> element, clone it, then append it to what LitElement calls renderRoot, which is normally the element’s shadow DOM, but can also be set to the element itself (this) for the inline (non–shadow DOM) case.

But wait! The content served up by the microapp server is an entire HTML page. We cannot insert the HTML page for the microapp, complete with html, head, and body tags, into the middle of the one for the macroapp, can we?

We solve this problem by taking advantage of a quirk of the template tag in which the microapp content retrieved from the microapp server is wrapped. It turns out that when modern browsers encounter a template tag, although they do not “execute” it, they do parse it, and in doing so they remove invalid content such as the <html>, <head>, and <body> tags, while preserving their inner content. So the <script> and <link> tags in the <head>, as well as the content of the <body>, are preserved. This is precisely what we want for purposes of inserting microapp content into our page.

Micro-frontend Architecture: The Devil Is in the Details

Micro frontends will take root in the webapp ecosystem if (a) they turn out to be a better architectural approach, and (b) we can figure out how to implement them in ways which satisfy the myriad practical requirements of today’s web.

In terms of the first question, no one claims that micro frontends are the right architecture for all use cases. In particular, there would be little reason for greenfield development by a single team to adopt micro frontends. I’ll leave the question of what types of apps in what types of contexts could benefit most from a micro-frontend pattern to other commentators.

In terms of implementation and feasibility, we have seen there are manifold details to be concerned with, including several not even mentioned in this article—notably authentication and security, code duplication, and SEO. Nonetheless, I hope that this article lays out a basic implementation approach for micro frontends which, with further refinement, can stand up to real-world requirements.

Bibliography

Understanding the basics

What are micro frontends?

Micro frontends are a new pattern where web application UIs (front ends) are composed from semi-independent fragments that can be built by different teams using different technologies. Micro-frontend architectures resemble back-end architectures where back ends are composed from semi-independent microservices.

What is micro-frontend architecture?

A micro-frontend architecture lays out the approach for the structural elements of a micro-frontend framework. It also defines the relationships among them, governing how UI fragments are assembled and communicate in order to achieve the optimal developer and user experience.

Can microservices have UIs?

Yes, in a sense they can. Micro-frontend patterns often adopt the approach where one fragment of the micro frontend, perhaps implemented as a micro-frontend web component, is paired with a microservice in order to provide its UI.

What defines a microservice?

A microservice is an element of an architecture in which applications are structured as a collection of interoperating services. If the front end adopts the micro-frontend pattern, a microservice may be paired with a micro frontend.

What is the relationship between micro frontends and web components?

Micro frontends and web components (custom elements) may be related in several ways. Web components are a natural markup-based way to describe the microapps composing a micro-frontend application. And the individual microapps in a micro frontend application may themselves be built using web components.