1. Introduction
To address the limitations in changes and expansion caused by massive monolithic backend services, Microservices emerged:
Microservices are a variant of Service-Oriented Architecture (SOA) that designs applications as a series of loosely coupled fine-grained services, organized through lightweight communication protocols
Specifically, build applications as a set of small services. These services can be independently deployed and scaled, each with solid module boundaries, even allowing different programming languages for different services, and can be managed by different teams
However, increasingly heavy frontend engineering also faces the same problems, naturally thinking of applying (copying) microservice ideas to the frontend, thus came the concept of micro-frontends:
Micro frontends, An architectural style where independently deliverable frontend applications are composed into a greater whole.
That is, an architectural style composed of multiple independently deliverable frontend applications. Specifically, decompose frontend applications into smaller, simpler chunks that can be independently developed, tested, and deployed, while still appearing to users as a single cohesive product:
Decomposing frontend monoliths into smaller, simpler chunks that can be developed, tested and deployed independently, while still appearing to customers as a single cohesive product.
2. Characteristics
Simply put, micro-frontend concepts are similar to microservices:
In short, micro frontends are all about slicing up big and scary things into smaller, more manageable pieces, and then being explicit about the dependencies between them.
Slice the massive whole into manageable small pieces, and clarify dependencies between them. Key advantages include:
-
Smaller codebases, more cohesive, higher maintainability
-
Loosely coupled, autonomous teams have better scalability
-
Gradual upgrade, update, or even rewrite of partial frontend functionality becomes possible
Simple, Loosely Coupled Codebases
Compared to a monolithic frontend codebase, codebases under micro-frontend architecture tend to be smaller/simpler, easier to develop
More importantly, avoid complexity escalation caused by unreasonable implicit coupling between modules. Reduce possibility of accidental coupling through clearly defined application boundaries, increase cost of logical coupling between sub-applications, prompting developers to clarify data and event flow in applications
Incremental Upgrade
Ideal code is naturally modular with clear dependencies, easy to extend, convenient to maintain... However, in practice for various reasons:
-
Legacy projects, ancestral code
-
Delivery pressure, sought speed at the time
-
Proximity and familiarity, sought stability at the time...
There always exist less-than-ideal code:
-
Outdated technology stacks, even forcibly mixing multiple technology stacks
-
Chaotic coupling, dare not touch, pulling one hair affects the whole body
-
Incomplete refactoring, refactoring-abandoned, change approach refactoring-abandoned again...
To thoroughly refactor this code, the biggest problem is difficulty having sufficient resources to make sweeping changes in one step, while gradually refactoring, must ensure intermediate versions can transition smoothly, while continuing to deliver new features:
In order to avoid the perils of a full rewrite, we'd much prefer to strangle the old application piece by piece, and in the meantime continue to deliver new features to our customers without being weighed down by the monolith.
Therefore, to implement gradual refactoring, we need incremental upgrade capability, first let old and new code coexist harmoniously, then gradually transform old code, until entire refactoring is complete
This incremental upgrade capability means we can perform low-risk partial replacement of product functionality, including upgrading dependencies, replacing architecture, UI redesign. On the other hand, also brings flexibility in technology selection, helpful for experimental trial-and-error of new technologies and interaction patterns
Independent Deployment
Independent deployment capability is crucial in micro-frontend systems, can reduce change scope, thereby reducing related risks
Therefore, each micro-frontend should have its own continuous delivery pipeline (including building, testing and deploying to production environment), and must be independently deployable, without过多 considering current state of other codebases and delivery pipelines:

Even if old system releases on fixed quarterly cycles or manually releases, even if neighboring team accidentally releases a half-finished or problematic feature, it doesn't matter. That is, if a micro-frontend is ready to release, it should be releasable anytime, and determined only by the team developing and maintaining it
P.S. Can even combine with BFF pattern to achieve further independence:

Team Autonomy
Besides decoupling on codebase and release cycle, micro-frontends also help form completely independent teams, with different teams each responsible for a product functionality from conception to release, teams can fully own everything needed to provide value to customers, thereby operating quickly and efficiently
For this, should organize teams vertically around business functions, not based on technical functions. Simply, can divide according to content end users can see, such as taking each page in application as a micro-frontend, and assigning to a team full responsibility. Compared to teams organized based on technical functions or horizontal concerns (such as styles, forms, validation, etc.), this approach can improve team work cohesion

3. Implementation Solutions
In implementation, key questions are:
-
How to integrate multiple Bundles?
-
How to isolate impact between sub-applications?
-
How to reuse public resources?
-
How to communicate between sub-applications?
-
How to test?
Multiple Bundle Integration
Micro-frontend architecture generally has a container application integrating sub-applications, responsibilities as follows:
-
Render common page elements, such as header, footer
-
Address cross-cutting concerns, such as authentication and navigation
-
Integrate various micro-frontends onto one page, and control micro-frontend rendering area and timing
Integration methods divided into 3 categories:
-
Server-side integration: such as SSR template assembly
-
Build-time integration: such as Code Splitting
-
Runtime integration: such as through iframe, JS, Web Components, etc.
Server-side Integration
Key to server-side integration is how to ensure each template (each micro-frontend) can be independently published, if necessary, can even establish a structure on server-side corresponding to frontend:

Each sub-service responsible for rendering and serving corresponding micro-frontend, main service initiates requests to various sub-services
Build-time Integration
Common build-time integration method is publishing sub-applications as independent npm packages, together as main application dependencies, building to generate a JS Bundle for deployment
However, biggest problem with build-time integration is causing coupling at release stage, any sub-application change requires entire recompilation, means even small product partial changes require releasing a new version, therefore, this method is not recommended
Runtime Integration
Postponing integration timing from build-time to runtime can avoid coupling at release stage. Common runtime integration methods include:
-
iframe
-
JS: such as frontend routing
-
Web Components
Although intuitively using iframe seems not good (performance, communication cost, etc.), but here it's indeed a reasonable option, because iframe is undoubtedly the simplest method, also naturally supports style isolation and global variable isolation
But this native isolation means difficulty connecting various parts of application together, routing control, history stack management, deep-linking, responsive layout, etc. all become abnormally complex, thus limiting flexibility of iframe solution
Another most common method is frontend routing, each sub-application exposes render function, main application loads independent Bundles of various sub-applications at startup, afterwards renders corresponding sub-applications according to routing rules. Currently appears to be most flexible method
Another similar method is Web Components, encapsulating each sub-application as custom HTML element (rather than render function in frontend routing solution), to gain benefits brought by Shadow DOM such as style isolation
Impact Isolation
Style and scope isolation between sub-applications, and between sub-applications and main application is a problem that must be considered, common solutions as follows:
-
Style isolation: development standards (such as BEM), CSS preprocessing (such as SASS), module definition (such as CSS Module), writing with JS (CSS-in-JS), and shadow DOM feature
-
Scope isolation: various module definitions (such as ES Module, AMD, Common Module, UMD)
Resource Reuse
Resource reuse has important significance for UI consistency and code reuse, but not all reusable resources (such as components) must be extracted for reuse at the beginning, recommended approach is allowing certain degree of redundancy in early stage, each Bundle creates components in respective codebases, until forming relatively clear component API then establish public components available for reuse
On the other hand, resources divided into following 3 categories:
-
Basic resources: icons, tags, buttons, etc. completely without logical functionality
-
UI components: search boxes with certain UI logic (such as autocomplete), tables (such as sorting, filtering, pagination), etc.
-
Business components: containing business logic
Among them, not recommended to reuse business components across sub-applications, because it causes high coupling, increases change cost
For ownership and management of public resources, generally two modes:
-
Public resources belong to everyone, i.e., no clear ownership
-
Public resources centrally managed, managed by dedicated personnel
From practical experience, former easily evolves into hodgepodge without clear standards and deviating from technical vision, while latter causes disconnection between resource creation and use, relatively recommended mode is open source software management mode:
Anyone can contribute to the library, but there is a custodian (a person or a team) who is responsible for ensuring the quality, consistency, and validity of those contributions.
That is, everyone can supplement public resources, but someone (or a team) responsible for supervision, to guarantee quality, consistency, and correctness
Inter-Application Communication
Indirect communication through Custom Events is a common way to avoid direct coupling, additionally, React's unidirectional data flow model can also make dependencies more explicit, corresponding to micro-frontends, passing data and callback functions from container application to sub-applications
Additionally, routing parameters besides being usable for sharing, bookmark, etc. scenarios, can also serve as a communication means, and have many advantages:
-
Its structure follows well-defined open standards
-
Page-level sharing, globally accessible
-
Length limits prompt passing only necessary small amount of data
-
User-oriented, helpful for domain modeling
-
Declarative, semantically more general ("this is where we are", rather than "please do this thing")
-
Forces indirect communication between sub-applications, not directly depending on each other
But in principle, regardless of which method adopted, should minimize communication between sub-applications as much as possible, to avoid strong coupling caused by大量 weak dependencies
Testing
Each sub-application should have its own complete test suite, special point is, besides unit tests, functional tests, also must have integration tests:
-
Integration tests: guarantee correctness of integration between sub-applications, such as cross-sub-application interactions
-
Functional tests: guarantee correctness of page assembly
-
Unit tests: guarantee correctness of underlying business logic and rendering logic
Form a pyramid structure from bottom to top, each layer only needs to verify parts not covered by its lower layer
4. Examples
-
Online Demo: https://demo.microfrontends.com/
-
Source code address: micro-frontends-demo/container
-
Detailed introduction: The example in detail
5. Disadvantages
Of course, this architectural pattern is not all benefit and no harm, some problems also come along:
-
Causes redundant dependencies, increases user traffic burden
-
Increased team autonomy may damage collaboration
Traffic Burden
Independent building means redundancy of public resources, thereby increasing user traffic burden
No very ideal solution, one simple solution is removing public dependencies from (sub-application) build artifacts, but this introduces build-time coupling:
Now there is an implicit contract between them which says, "we all must use these exact versions of these dependencies".
Operational/Management Complexity
Before adopting micro-frontends, first consider several questions:
-
How to extend existing frontend development, testing, release processes to support many applications?
-
Are dispersed, weakly controlled tool systems and development practices reliable?
-
For various frontend codebases, how to establish quality standards?
In short, different from before, micro-frontends will produce a bunch of small things, therefore need to consider whether possessing technical and organizational maturity required for adopting this method
6. Summary
Similar to microservices for backend, after frontend business develops to a certain scale, also needs an architectural pattern to decompose complexity, thus emerged application of microservice ideas in frontend domain, i.e., micro-frontends. Main purposes include:
-
Further scalability in technical architecture (clear module boundaries, explicit dependencies)
-
Autonomy in team organization
-
Can independently develop and independently deliver in development process
Greatest significance lies in unlocking capability for multiple technology stacks coexistence, especially suitable for architecture upgrade transition period in gradual refactoring:
Suddenly we are not tightly coupled with one stack only, we can refactor legacy projects supporting the previous stack and a new one that slowly but steadily kicks into production environment without the need of a big bang releases (see strangler pattern).
Allows low-cost trial of new technology stacks, even allows selecting most suitable technology stack for different things (similar to allowing different languages for different services in microservices):
we can use different version of the same library or framework in production without affecting the entire application, we can try new frameworks or approaches seeing real performances in action, we can hire the best people from multiple communities and many other advantages.
References
-
[I don't understand micro-frontends.](https://medium.com/ @lucamezzalira/i-dont-understand-micro-frontends-88f7304799a9)
-
[Micro frontends—a microservice approach to front-end web development](https://medium.com/ @tomsoderlund/micro-frontends-a-microservice-approach-to-front-end-web-development-f325ebdadc16)
No comments yet. Be the first to share your thoughts.