Microfrontends: Microservices for the Frontend
Microservices are a popular way to build small, autonomous teams that can work independently. Unfortunately, by their very nature, microservices only work in the backend. Even with the best microservice architecture, frontend development still requires a high degree of interdependence, and this introduces coupling and communication overhead that can slow down everyone.
Can we take microservice architecture patterns and apply them to the frontend? It turns out we can. Companies such as Netflix, Zalando, and Capital One have pushed the pattern to the front, laying the groundwork for microfrontends. This article will explore microfrontends, their benefits and disadvantages, and how they differ from traditional microservices.
Microservices for the Frontend
Microfrontends are what we get when we bring the microservice approach to the frontend. In other words, a microfrontend is made of components — owned by different teams — that can be deployed independently. These components are assembled to create a consistent user experience.
With a microfrontend, no single team owns the UI in its entirety. Instead, every team owns a piece of the screen, page, or content. For example, one team might be responsible for the search box, while another might code suggestions based on users’ tastes. Additional teams might code the music player, manage playlists, or render the billing page. We add complexity but teams get increased autonomy in return.
Benefits and Challenges of Microfrontends
Microfrontends offer similar benefits to microservices. Namely, we can scale up development by breaking the frontend code into isolated pieces, for which different teams are responsible. As with microservices, each feature can be released on its own, at any time, with little coordination. This leads to more frequent updates.
Vertical Teams
Microfrontends enable the creation of vertical teams, which means a full-stack team of developers can own a feature on both the backend and the frontend at the same time.
Continuously Deployable Components
Every part of a microfrontend is a deployable unit. This allows teams to publish their changes without waiting for a release train or depending on other teams finishing their work. The end result is that the frontend can be updated several times per day.
Challenges of Microfrontend Design
The main challenge of microfrontends is creating a fast and responsive client. We must never lose sight of the fact that the frontend lives in an environment with limited memory, CPU, and network, or we risk ending up with a slow UI.
A snappy UI is vital for the success of the product. A recent survey pointed out that “a site that loads in 1 second has a conversion rate 3x higher than a site that loads in 5 seconds”. Every second the user has to wait, money is thrown out of the window.
In addition to all the challenges microservices have, microfrontend design poses a few more problems:
Isolation: each team’s code must eventually coexist on the same browser. We must be deliberate in isolating the separate modules to avoid collisions of code or style.
Shared resources: to avoid duplication and keep the frontend thin, components should share assets and libraries when possible, which may create undesirable coupling.
Accessibility: heavy reliance on JavaScript to render the page negatively affects accessibility.
Styling: When the UI is made up of components produced by various teams, maintaining a consistent look is more complicated. Small stylistic inconsistencies can feel jarring.
Coordination: with so many moving parts, APIs need to be very well-defined and stable. Teams must coordinate on how different components in the microfrontend communicate with each other and the backend microservices.
Principles for Building Microfrontends
There are two complementary methods for rendering a unified UI from separate microfrontend components: server-side and client-side rendering.
Server-Side Rendering (SSR)
Server-side rendering offers faster performance and more accessible content. Rendering on the server is a good alternative for serving content quickly — especially on low-power devices like low-end phones. It is also a suitable fallback mode when JavaScript is disabled.
We have a few ways of performing SSR:
Server Side Includes (SSI): is a simple scripting language executed by the webserver. The language uses directives to build HTML fragments into a full page. These fragments may come from other files or the responses of programs. SSI is supported by all major webservers, including Apache, Nginx, and IIS.
iframes: the venerable iframe feature allows us to embed arbitrary HTML content on a page.
Edge Side Includes (ESI): a more modern alternative to SSI. ESI can handle variables, have conditionals, and supports better error handling. ESI is supported by caching HTTP servers such as Varnish.
So, for instance, we can use SSI to render a page from HTML with this:
<div>
<!--#include virtual="/hello-world" -->
</div>
The virtual
keyword makes the webserver request the content from a URL or CGI program. In our case, we would need to set up the webserver to respond to requests based on the path /hello-world
with a suitable fragment:
<h1>Hello World!</h1>
SSR is used in many web frameworks to render the first screen. Additionally, there are also some interesting SSR-specific utilities like compoxure, nodesi, and tailor.
Client-side Rendering (CSR)
Client-side rendering builds the page in the user’s browser by fetching data from microservices and manipulating the DOM. Most web frameworks use some form of CSR to improve the user's experience.
Custom elements are dynamically mounted and unmounted from the page using JavaScript:
// hello-world-component.js
class HelloWorld extends HTMLElement {
connectedCallback() {
this.innerHTML = `<h1>Hello world</h1>`;
}
}
customElements.define('hello-world', HelloWorld);
Once defined, we can use the new element like any other HTML tag:
<hello-world></hello-world>
In the example, the full page would include a script tag to fetch the JavaScript components:
<!doctype html>
<head>
<meta charset="utf-8">
<title>Microservice Example</title>
</head>
<body>
<script src="./hello-world-component.js" async></script>
<hello-world></hello-world>
</body>
While most frontend frameworks can be used for microfrontends, some have been designed specifically for them:
Piral: implements isolated components called pilets. Pilets are modules that bundle content and behavior.
Ragu: a framework of frameworks. It allows us to embed code written in any framework as widgets.
Single SPA: a meta-framework for piecing a UI together using any combination of frontend frameworks like React, Angular, and Ember, among others.
Frint: another modular framework for building component-based applications. Integrates with React, Vue, and Preact.
- Module Federation: a WebPack plugin to create Single Page Applications (SPAs) by bundling separate builds. These builds can be developed independently of each
Conclusion
Switching to a microfrontend architecture can give our development teams more autonomy, thereby accelerating development. However, the same caveats that apply to microservices are also relevant for microfrontends. We need to have a proven design, which means microfrontends are not a good fit for greenfield projects.
New projects are best served by traditional patterns such as single page applications (SPAs) managed by a single team. Only once the frontend has stood the test of time can we consider microfrontends as the way forward.