December 2023
An architectural style where independently deliverable frontend applications are composed into a greater whole
The concept of micro frontends evolved organically rather than being the invention of a single individual. It emerged as a natural extension of microservices architecture, adapting its principles to frontend development. Microservices were popularized by companies like Netflix and Amazon, which demonstrated the effectiveness of breaking down backend systems into smaller, independently deployable services.
The term "micro frontends" started gaining traction in the tech community around 2016-2017. Thought leaders and practitioners in software development, such as Martin Fowler and others, began discussing and formalizing the concept.
Core concepts
by Luca Mezzalira
Micro-frontend decision framework
by Luca Mezzalira
Definition: In build-time integration, micro frontends are combined into a single application bundle during the build process. This happens before the application is deployed.
Advantages:
Definition: In build-time integration, micro frontends are combined into a single application bundle during the build process. This happens before the application is deployed.
Challenges:
Definition: In run-time integration, micro frontends are assembled during the execution of the web application in the user's browser. Each micro frontend is loaded independently.
Advantages:
Definition: In run-time integration, micro frontends are assembled during the execution of the web application in the user's browser. Each micro frontend is loaded independently.
Challenges:
Using iframes is one of the techniques to implement micro frontends, especially when isolation and independence are key concerns. An iframe, short for inline frame, is an HTML element that allows an external webpage to be embedded within the current page. Here's how iframes are used in the context of micro frontends:
Isolation: iframes provide a strong level of isolation between the host page and the embedded micro frontend. This isolation includes JavaScript execution, CSS styling, and local storage, which helps prevent conflicts and ensures security.
Independence: Each micro frontend can be developed, deployed, and operated independently. Changes in one micro frontend do not affect others, as each is loaded in its own iframe.
Technology Agnosticism: Teams can use different technology stacks for each micro frontend, as the iframe acts as a container that doesn't interfere with the host page's technology.
Simplified Integration: Embedding a micro frontend is as simple as pointing the iframe's src attribute to the URL of the micro frontend. This reduces the complexity of integration compared to other methods.
Performance Overhead: Loading a webpage in an iframe can be slower compared to other integration methods, as each iframe is essentially a separate webpage with its own resources and assets.
User Experience Consistency: Ensuring a consistent look and feel across the host page and the iframes can be challenging. Additionally, navigation and interactions might feel less seamless compared to a single-page application.
Cross-iframe Communication: Communication between the host page and iframes or between different iframes can be complex, often relying on postMessage API or similar mechanisms. This can add overhead and complexity in handling data exchange and synchronization.
SEO and Analytics Considerations: Iframes can complicate search engine optimization (SEO) and analytics, as search engines might treat the content in iframes differently from the main page.
Accessibility: Making iframe content accessible can be challenging, as screen readers and other assistive technologies may not always interact well with iframes.
Module Federation
Module Federation is a feature provided by Webpack 5 and recently by Vite as well. It allows for the dynamic sharing of code and functionality between separate and independently deployed JavaScript applications at runtime. This feature is particularly relevant in the context of micro frontends, as it enables different frontends (or parts of a frontend) to function as standalone applications yet still share code seamlessly.
Module Federation
Host and Remote: In Module Federation, there are typically two roles:
Dynamic Code Sharing: Unlike traditional static imports, Module Federation enables dynamic loading of code from a remote application. This means that the host application can import modules from a remote application at runtime.
Version Management: It allows different versions of the same shared modules to coexist in the same application, helping to avoid version conflicts.
Isolation and Independence: Each part of the application (whether host or remote) can be developed, deployed, and operated independently, reducing the risk of changes in one part affecting the others.
Module Federation
Module Federation
// webpack.config.js of Remote
plugins: [
new ModuleFederationPlugin({
name: 'remoteApp',
filename: 'remoteEntry.js',
exposes: {
'./MyComponent': './src/MyComponent',
},
shared: { react: { singleton: true }, 'react-dom': { singleton: true } },
//shared: ["react", "react-dom"]
}),
]Module Federation
// webpack.config.js of Host
plugins: [
new ModuleFederationPlugin({
remotes: {
remoteApp: 'remoteApp@http://example.com/remoteEntry.js',
},
shared: { react: { singleton: true }, 'react-dom': { singleton: true } },
}),
]Module Federation
// In a component of the Host application
import MyComponent from 'remoteApp/MyComponent';
function App() {
return (
<div>
<h1>Host Application</h1>
<MyComponent />
</div>
);
}But what is Webpack btw?
Webpack is a powerful and popular open-source JavaScript module bundler, but its capabilities extend far beyond just bundling. It's a tool that's primarily used to compile JavaScript modules, and it has become an essential part of the web development workflow, particularly in complex applications. Here's a detailed explanation of Webpack:
But what is Webpack btw?
Loaders: Webpack itself only understands JavaScript. Loaders transform other types of files into modules that Webpack can handle. For example, style-loader and css-loader enable Webpack to handle CSS files, babel-loader allows you to use modern JavaScript (ES6+) by converting it into a format that different browsers can understand.
Plugins: Plugins can be used to perform a wider range of tasks like bundle optimization, asset management, and injection of environment variables.
Entry Point: This is where Webpack starts the bundling process. It represents the module/file from which Webpack starts building its internal dependency graph.
Output: It's the bundled JavaScript file(s) that Webpack generates. The configuration specifies where to emit these bundles and what to name them.
Mode: Webpack can be run in different modes, namely development, production, or none. Each mode has a set of default optimizations.
DevServer: Webpack provides a development server that can be used to serve web applications for development purposes, featuring live reloading.
Alternatives to webpack:
There are many ways to aquire routing inside micro-frontends. The obvious choice would be some Nginx config.
In React environment we can use React Router Dom library
// App.js (Container Application)
import { BrowserRouter as Router, Routes, Route } from 'react-router-dom';
import MicroFrontendA from './MicroFrontendA';
import MicroFrontendB from './MicroFrontendB';
function App() {
return (
<Router>
<Routes>
<Route path="/micro-frontend-a/*" element={<MicroFrontendA />} />
<Route path="/micro-frontend-b/*" element={<MicroFrontendB />} />
// Add more routes for other micro frontends
</Routes>
</Router>
);
}
export default App;Container app
// MicroFrontendA.js
import { BrowserRouter as Router, Routes, Route } from 'react-router-dom';
import PageOne from './PageOne';
import PageTwo from './PageTwo';
function MicroFrontendA() {
return (
<Router>
<Routes>
<Route path="/" element={<PageOne />} />
<Route path="/page-two" element={<PageTwo />} />
</Routes>
</Router>
);
}
export default MicroFrontendA;Microfrontend app
Ensuring that internal links and routes in micro frontends are relative to the micro frontend's base path is crucial for correct routing, especially when these micro frontends are integrated into a larger application. Here’s how to manage base paths in React Router DOM v6 for micro frontends:
BrowserRouter with a basename
When you use BrowserRouter in your micro frontend, you can specify a basename. This basename is the base URL for all locations. For example, if your micro frontend is served from /micro-frontend-a, you set the basename to /micro-frontend-a.
// MicroFrontendA.js
import { BrowserRouter as Router, Routes, Route } from 'react-router-dom';
function MicroFrontendA() {
return (
<Router basename="/micro-frontend-a">
<Routes>
<Route path="/" element={<HomePage />} />
<Route path="/about" element={<AboutPage />} />
{/* other routes */}
</Routes>
</Router>
);
}
export default MicroFrontendA;Comunication in MF
Comunication in MF
Comunication in MF
Comunication in MF
Comunication in MF
Comunication in MF
Comunication in MF
Comunication in MF
How to start with MF?
How to start with MF?
How to start with MF?
The only thing a Big Bang rewrite guarantee is a Big Bang.
M. Fowler
How to start with MF?
The Strangler Pattern, named after the strangler fig that gradually envelops and replaces a host tree, is a method used in software development for incrementally migrating a legacy system to a new system. In the context of micro frontends, this pattern is particularly useful for gradually transitioning a large, monolithic frontend into a more modern, scalable set of micro frontends. This approach allows for a smooth and progressive replacement rather than a risky and disruptive big-bang overhaul.
Identify Replaceable Parts: Begin by identifying parts of the existing monolithic frontend that can be replaced with micro frontends. These parts could be specific features, pages, or components.
Develop New Micro Frontends: Develop new micro frontends to replace the identified parts of the old system. These micro frontends are developed and deployed independently, often using modern technologies and practices.
Route Requests to New System: Use routing logic to direct requests to the appropriate system – either the new micro frontend or the existing monolithic application. This can be done using a reverse proxy, a frontend router, or similar mechanisms.
Incremental Replacement: Gradually replace more components of the old system with new micro frontends. As new features are built or existing features are refactored, they are implemented as separate micro frontends.
Retire Old System: Eventually, as more of the system is replaced, the old monolithic frontend becomes redundant. Once all its parts have been replaced and functionality is verified, it can be decommissioned.
The Reverse Strangler Pattern in the context of micro frontends is a variation of the traditional Strangler Pattern, but with a focus on gradually integrating legacy systems into a new, modern architecture, rather than replacing the old system entirely. This approach is particularly useful when certain parts of the legacy system are still valuable, viable, and not feasible to replace in the short term. Instead of strangling the old system, the new system wraps around and integrates with it.
Build a New System Around the Legacy: Start by building a new micro frontend architecture around the existing legacy system. The new system initially acts as a shell or a facade that interfaces with the legacy system.
Gradual Integration: Instead of replacing legacy components, gradually integrate them into the new architecture. This might involve wrapping legacy functionality with new interfaces or building bridges that allow the new and old systems to communicate seamlessly.
Incremental Enhancement: Enhance and extend the functionality of the legacy system within the new architecture. This might involve adding new features as micro frontends that work in conjunction with the existing system.
Refactoring Legacy Components: Over time, selectively refactor parts of the legacy system to better fit into the new architecture, improving them or adapting their interfaces.
Full Integration or Eventual Replacement: Eventually, the legacy system becomes fully integrated into the new architecture, either remaining as a functional part of the ecosystem or being replaced piece by piece, as and when it makes sense.
React – loosly coupled
The question is if each of microfrontends in the network should or should not assume they use React.
//Remote MF
export const mount = (element) => {
const App = () => <h1>Hello, React!</h1>;
if (element) {
createRoot(element).render(<App />);
}
};React – loosly coupled
The question is if each of microfrontends in the network should or should not assume they use React.
//Container MF
import { mount } from 'remote/remote';
import React, { useRef, useEffect } from 'react';
export default () => {
const ref = useRef(null);
useEffect(() => {
mount(ref.current);
});
return <div ref={ref} />;
};React – tightly coupled
In this case you can assume that both parts uses React so its fine the import components across multifrontedend network.
MicroFrontends antipatterns
There is no compression algorithm for experience
MicroFrontends antipatterns
Don't mix components and microfrontends
Microfrontend is typically a subdomain of the large application owned by one team.
I allows the team to mentally comprehend the complexity of the subdomain.
Microfrontend should be an independent app in terms of functionality or at least should be deployed and maintained independently.
Tip: You shouldn't have too many microfrontends
MicroFrontends antipatterns
Don't mix too many technologies and frameworks in microfrontends network, unless you really should.
From the perspective of the user we have just one app and it should be consistent.
Reasons to have multiframework approach:
1. legacy code
2. teams with different skills
3. migrations
MicroFrontends antipatterns
Dependency hell
Don't share all the dependencies across the network
MicroFrontends antipatterns
Bidirectional data binding
MicroFrontends antipatterns
Global State
Use EventEmitter instead of global state.
The system should be loosly coupled by highly aligned
MicroFrontends antipatterns
Multiple api calls
Try to prevent multiplying requests across multifrontend network