Related articles in the Micro Frontend series:
Is Micro Frontend just another library or framework?
Just three years ago, if you wanted to integrate a Micro Frontend into your architecture, there was not an off-the-shelf npm library or framework you could apply directly and expect it to work for you out-of-the box. Since then, the Micro Frontend concept has been gaining a lot of popularity among companies that don’t want to continue building and sustaining a frontend monolith. Thanks to the increase in popularity, there are now a variety of online resources or implementation examples on npm and GitHub.
Regardless of which library, framework, or tools you would use to implement the Micro Frontend, it is important to understand that the implementation varies depending on your requirements. We will cover some of the consideration points that can help you formulate the appropriate Micro Frontend requirements.
Technical Considerations
How do you accelerate the feature development, build, and deployment workflow with a Micro Frontend?
Comparison of frontend code organization
To accelerate feature development with frontend code changes and deployment workflows, we need to first examine how our frontend code is organized within our code repositories and how they can be integrated at build time or runtime. Here are some of the ways codes can be organized and how they compare across different approaches.
Micro Frontend build / loading approaches | Frontend Monolith | Micro Frontend with Build-time Integration | Micro Frontend with runtime integration |
Integration Approach | No integration. One code repository with everything in it. | A root application that npm installs on each of the web applications | A root application that dynamically loads each independently deployed web applications |
Difficulty to set up | Easy | Medium | Advanced |
Separate code repositories | No | No | Yes or No |
Separate builds | No | Yes | Yes |
Separate deployments | No | Yes | Yes |
Advantages | Simple | Each web application can be built separately before publishing to npm | Supports an independent Micro Frontend deployment and release without any dependencies. Incredibly scalable. |
Disadvantages | Slow build because every piece moves at the speed of the slowest part. Deployments are all tied together | Root application needs to reinstall, rebuild, and redeploy whenever one of the web applications changes. | Requires knowledge of the relationship between the web app shell (container app) and the Micro Frontends that will be consumed by web app shell. |
We want to try something different than the frontend monolith. One way of bootstrapping different web application is to have each Micro Frontend built separately and integrated together at build time from the root (or container) application (Table one – Micro Frontend with Build-Time Integration). However, the drawback here is that every time we need to upgrade certain Micro Frontends, the root application itself needs to be rebuilt and redeployed. Ideally, we do not want to hold strong dependencies on other components at build time or runtime, therefore, the third approach is to have a completely decoupled development, build and deployment workflow and have Micro Frontend bootstrapped at runtime (Table one - Micro Frontend with Runtime Integration).
How is workflow productivity improved?
To understand how the Micro Frontend approach could help accelerate the build and deployment workflow, imagine there is one or a few frontend monoliths that are built within Product X of your organization—the build and deployment flow may look very similar to Figure 1 below.
As Figure 1 has illustrated, adding and building a new UI page for the frontend monolith involves multiple teams working on the same repository and depends on the same build/deployment workflow whenever there is a change.
With the Micro Frontend architecture, different development teams are decoupled completely in the development, build, and deployment workflow (Figure 2). We want to be able to ship with the different deployment pipelines and release different Micro Frontends for our web application with a non-blocking model, meaning that new features will not be sitting at idle state, waiting for something to happen beyond the team’s control.
Teams can remain autonomous and work on a separate code repository, own their CI/CD pipeline and release, and deploy at the cadence that is independent from other teams. After teams are provided with self-service tools and the automation capabilities they need to implement the build and deployment pipelines, the development workflow will become more productive and agile so it can produce an unimpeded, steady flow of feature delivery.
How do microservices and Micro Frontends work together?
Technically, the Micro Frontend gels well with microservices because communication between the two are contracted and funneled with APIs (such as REST or GraphQL) over the network, or via a backend for frontend (BFF) service that aggregates the data needed for presentation from other upstream backend services.
Different microservices and Micro Frontends could represent different part of business domain or services and owed by different teams (Figure 3). Essentially, these teams can be working on independent features or any part of web application, but the presentation will be a cohesive user interface with a top-level web application shell (or container) that take cares of heavy lifting in orchestrating the Micro Frontends it manages. For example, similar to how each microservice controls their own database, each Micro Frontend only controls the browser document object model (DOM) that it owns. The top-level web application shell oversees the DOM in the browser as a shared resource and facilitates how each Micro Frontend should be mounted, displayed, and unmounted at run time.
Who owns the microservices or the Micro Frontend stack?
Ideally, after each team have gained the needed skills and knowledges, they can own and operate the entire stack powered by both microservice and the Micro Frontend architecture (Figure 3: team boundaries and ownership). Such teams are referred to as “stream-aligned teams” and work on the full spectrum of delivery. The advantages of being stream-aligned teams that own the full stack are:
- Minimal handing off work to other teams, reducing wait time between teams
- Quickly incorporates feedback from customers
- Reacts frontend and backend system problems
What would the integrated Micro Frontend look like?
The final look and feel of the integrated Micro Frontend website should look no different than any other website you would build with frontend monolith. However, the composition of the underlying pages or components would be bootstrapped differently. In other words, if user cannot tell the difference, you know you have succeeded.
Consider a common UI layout on a website with a shared header at the top and a main content section below. The website will be integrated with a myriad of service offerings, which are in turn a Micro Frontend with its own tech stack (Figure 4)
The example website will respond to different routing requests from the browser, and then mount the corresponding Micro Frontend to be rendered in the main section of the DOM.
Essentially, there is a transparent web application shell (or container application) that is responsible of:
- Locating and discovering the available Micro Frontend
- Composing a Micro Frontend on the UI with common elements like shared header or footer
- Managing the lifecycle (like when to mount/unmount from browser DOM) of each Micro Frontend based on conditions like the routing event
- Facilitating use cases that can be handled in a common, sharable context, such as a sharable data and communication channel, feature flags, loader component, locale, and UI theme.
How can a Micro Frontend be discovered and bootstrapped?
The web application shell (or container application) will play a role of loading the corresponding Micro Frontend at the desired location on the website (such as in the browser DOM). To achieve this, we need a way to programmatically locate, resolve, and initialise the JavaScript modules at run time.
Conceptually, in the browser at run-time, the web application shell needs to download and execute the JavaScript code from dynamic URLs with content served by different origins such as the content delivery network (CDN) or from content servers like Nginx. This approach helps you align with your goal of using distributed hosting from having an independent frontend deployment using different code repositories to minimize the development and release dependencies.
There are tools like module federation from Webpack 5 and browser specifications like Imported Maps, or Polyfill.io support for import maps like SystemJS or es-module-shims that could help facilitate loading remote modules on the browser at runtime. Nevertheless, in the situation that your user’s browser does not support the aforementioned or isn’t using Webpack 5 as a bundler, you can use “vanilla” JavaScript for the design of the manifest file that serves as the "public interface" for the Micro Frontend and contains information about the location of web assets, scripts, CSS, etc (Figure 5).
Once the web app shell consumes the given manifest file (such as app-manifst.json from Figure 5), it can help load the required JavaScript chunk files and bundled files exposed from the given Micro Frontend by either creating a new <script> tag in the browser DOM or programmatically with dynamic import.
Allowing Micro Frontend to be lazy loaded by the web application shell
After the web application shell proceeds and locates the Micro Frontend bundles and remembers these script files, it still needs to "boot up" the Micro Frontend or dynamically upload the appropriate location in the browser’s DOM tree. This is technically referred to “lazy loading” or “lazy mounting.” Such Micro Frontend mounting, bootstrapping, and unmounting can be orchestrated by web application shell with JavaScript or by using meta-frameworks such as the Single-SPA framework. By leveraging the Single-SPA framework, the web application shell can help register each Micro Frontend with the appropriate target DOM location (such as <main id=app>) to ensure that the right Micro Frontends are active (mounted) or inactive (unmounted) within the <main> tag for any particular URL/route defined (Figure 6).
Continuing with Single-SPA example, each Micro Frontend needs to implement their lifecycle functions by defining the actual implementation for how to bootstrap/mount/unmount components to the DOM tree with JavaScript or a different flavour of the JavaScript framework. For example, there are helper libraries available that support a variety of JavaScript frameworks like React, Vue, Angular, Ember, etc. This enables you to adapt the different JavaScript framework so it can be supported in the Micro Frontend architecture.
In the process of this change, the existing Micro Frontend SPA becomes an HTML-less application because it is no longer the Micro Frontend’s job to manage how it would be eventually rendered to the target DOM element in HTML. This rendering job has been delegated to the web application shell to ensure that each registered Micro Frontend gets mounted and rendered in the appropriate DOM element. Essentially, there is just one HTML file for the entire website via the web application shell.
How to handle page transition routing
This section examines to examples for how page transitions occur while the user navigates the Micro Frontend enabled website.
Top-level router and page fragments
This approach involves integrating page fragments to have a common HTML shell and implementing a top-level router (the mapping URLs). The URL mapping will dictate the routing behaviour when transitioning between different page fragments.
However, depending on how the routes are discovered or integrated, it may introduce share code between teams. This means that if you update one router map, it will need to talk to all teams.
Server Side Includes (SSI)
This approach of routing between page fragment can be implemented on server-side with a SSI plug-in or Edge Side Includes (ESI) tags, such as the HTML page is assembled with HTML fragments. While such server-side template composition technically works well with SEO, it can’t load the new Micro Frontend for the client without a full page reload at most of the time.
Client-Side iFrame
The same concept can also be implemented with client-side iFrame to help compose the UI page with independent iFrame pages based on the given route.
The iFrame provides a sandboxing behaviour with a decent level of isolation and can even load content across domain. iFrames that exist under the same domain can access the browser storage (such as localStorage and sessionStorage) with values set by the web app shell or other Micro Frontend.
In the scenario you may need to display the server-rendered UI from the legacy web application built with .php, .jsp, and/or .asp. It also needs to co-exist with the new client-rendered Micro Frontend built with the modern JavaScript framework. The iFrame would help with loading and rendering the server-rendered HTML, while the other parts of page are client-side rendered.
Despite the usefulness of iFrame, it does come with some downsides:
- Top-level routing cannot be propagated to the iFrame page without extra work
- Challenging to synchronize with browser history, deep linking with bookmark use cases
- Debugging a problem is much more difficult. For example, a bug can’t be replicated when a page is within an iFrame but is loaded without an issue when outside the iFrame
- Communication across iFrames requires a special channel such as window.postMessage()
- Each time content in the iFrame needs to change, it will trigger a complete reload
- General issues with the scrollbar for the overflow layout
Top-level router and app-specific router
In this model, each team that builds a Micro Frontend maintains its own routing within the web application, and the top-level router implemented by a web application shell or container app handles the Micro Frontend web application switch (Figure 8).
Each Micro Frontend doesn’t need to worry about the top level routing nor should it be interfering with the global routing, because the web application shell take cares of the top level app switching.
On the other hand, top level inter-web-app routing managed by the web application shell does not interfere with each Micro Frontend's own internal inter-application routing, which should always be self-managed.
This approach can be implemented programmatically on the client side with JavaScript, which will help ensure smooth web app transitions on the same page without needing to refresh the entire page.
Micro Frontend routing best practices:
- Each Micro Frontend is responsible for inter-web-app routing between the pages/URL contained within its own SPA
- Once your Micro Frontend has been unmounted, make sure that there is no Micro Frontend specific route been injected or remains at the top-level URL. This could cause route pollution across all Micro Frontends and breaks the loading of different Micro Frontends
The advantages of this routing approach are:
- Clear separation of concerns
- Web app shell is only concerned with top level routing
- Each Micro Frontend can implement its own app-specific routing to support the sub-pages it needs to render
- Does not require server-side support for composition
- Less coupling on the routing logic. Each Micro Frontend can continue grow its own routing support without impacting others, provided that there is a good path naming scheme to prevent collision
How can you support the development of separate UIs while maintaining a cohesive, consistent user experience?
Shared style guide, design system, and CSS
To ensure there is a consistent look and feel across the user interfaces from different service teams, the standardized UX design system (Figure 3) is highly recommended. This will help promote code reuse and reduce the chance of duplicated work at the UI component level.
If there is a need to share the same style at the global level, then you can use global CSS classes, or CSS custom properties (aka CSS variables). However, sharing CSS globally can introduce risks to the class collision, because there is no module system, name spacing, or encapsulation. This problem can be exacerbated with a Micro Frontend setup because CSS can be written by different teams at various times in separate repositories, making it hard to track down.
Micro Frontend specific CSS
While Micro Frontends could share the same style guide and design system to create a cohesive UX, the non-sharable styling and its implementation is done with CSS that is specific or scoped to each Micro Frontend. This ensure class names of styling rules do not collide between Micro Frontends.
Scoping CSS to the Micro Frontend, in other words, an encapsulation technique to have the CSS only apply to one Micro Frontend or UI component, can be accomplished with variety of approaches and tools, including:
- Manually prefixed CSS classes with a unique string such as your web app’s name
- Bundle tools like Webpack to suffix all CSS classes or data attribution with a unique hash
- CSS modules or SASS modules
- CSS in JavaScript libraries that come with the built-in scoped CSS feature
- Shadow DOM with style isolation
Regardless of the styling technique you use, the goal is to ensure that each Micro Frontend has the autonomy it needs to style its UI and only allow desirable styles to be applied as intended without impacting other Micro Frontend.
What’s next?
In this article, we covered a number of technical considerations with illustrated examples to help you design and develop your Micro Frontend architecture.
It is important to remind yourself that there isn’t just one approach to implement a Micro Frontend, rather, there are many ways to accomplish the same goal with different trade-offs. Below is a table of acceptance criteria that serves as a good reference for you to help evaluate your own approach.
|
Separate Team Ownership |
|
Separate Code Repositories |
|
Separate Builds |
|
Separate Deployments |
|
Run Independently |
|
Technology Agnostic |
|
Corporate Identity |
|
Fast Loading |
|
Sharing Basic Layout |
|
UX Style Consistency |
|
Cohesive User Experience |
|
Modular for Reuse |
|
Smooth Inter-App Transition |
|
Inter-App Communication |
|
Troubleshooting Effort |