Why Micro Frontends Are Changing Large-Scale Frontend Architecture
This article explains how micro frontends, inspired by micro‑service principles, break down monolithic front‑end applications into independent sub‑applications, covering design motivations, practical implementation steps, sandbox isolation techniques, and code examples to improve maintainability and performance.
1. Introduction: What is a microservice?
Microservices have become a hot topic in recent years. From a front‑end perspective, we can view a system composed of PC Web, mobile H5, and an admin portal as a set of services.
Problems of a monolithic front‑end include:
Large codebase makes rapid onboarding and build difficult.
Duplicate APIs across systems.
Complex database design.
Microservices solve these by splitting the system into independent services managed via gateways and controllers.
In the front‑end world, micro‑frontend adopts the same idea: decouple logic by splitting services.
2. Front‑end Microservice Design
2.1 Why does the front‑end need microservices?
Day 1: build 20 s
Week 1: build 1 min
Month 1: build 5 min
As the project grows, a single large application becomes hard to maintain, leading to low development efficiency and difficult deployments.
2.2 Micro‑frontend application scenarios
A typical management system has many tabs and nested routes, which become hard to maintain over time.
With micro‑frontend, each tab becomes an independent sub‑application with its own state, scope, and bundle, managed by a single master application.
In short: Application routing → route dispatches application.
2.3 Early micro‑frontend approach – iFrame
Why not iframe?
Using an iframe can dynamically load sub‑applications by changing its src attribute.
Advantages:
Self‑contained styles.
Sandbox isolation.
Independent execution.
Drawbacks:
CSS synchronization issues.
Complex communication (postMessage).
Cannot share components.
Potential performance and memory overhead.
Modern micro‑frontend designs aim to keep iframe benefits while solving these problems.
3. Core Logic of Micro‑frontend
3.1 Sub‑application Loading (Loader)
Typical flow:
First load the master, then let the master decide which registered sub‑application to load, finally render the sub‑app via vue‑router or react‑router.
3.1.1 Registration
// Assume our micro‑frontend framework is called hailuo
import Hailuo from './lib/index';
// 1. Declare sub‑applications
const routers = [
{
path: 'http://localhost:8081',
activeWhen: '/subapp1'
},
{
path: 'http://localhost:8082',
activeWhen: '/subapp2'
}
];
// 2. Register sub‑applications
Hailuo.registerApps(routers);
// 3. Run micro‑frontend
Hailuo.run();Registration simply stores an array of sub‑apps.
registerApps(routers: Router[]) {
(routers || []).forEach(r => {
this.Apps.push({
entry: r.path,
activeRule: location => location.href.indexOf(r.activeWhen) !== -1
});
});
}3.1.2 Intercept
We intercept routing events to control the timing of main/sub‑app logic.
import Hailuo from "./";
const EVENTS_NAME = ['hashchange', 'popstate'];
const EVENTS_STACKS = { hashchange: [], popstate: [] };
const handleUrlRoute = (...args) => {
// Load the corresponding sub‑app
Hailuo.loadApp();
// Execute sub‑app route handlers
callAllEventListeners(...args);
};
export const patch = () => {
window.addEventListener('hashchange', handleUrlRoute);
window.addEventListener('popstate', handleUrlRoute);
const originalAddEventListener = window.addEventListener;
const originalRemoveEventListener = window.removeEventListener;
window.addEventListener = (name, handler) => {
if (name && EVENTS_NAME.includes(name) && typeof handler === 'function') {
if (!EVENTS_STACKS[name].includes(handler)) {
EVENTS_STACKS[name].push(handler);
}
return;
}
return originalAddEventListener.call(this, name, handler);
};
window.removeEventListener = (name, handler) => {
if (name && EVENTS_NAME.includes(name) && typeof handler === 'function') {
const idx = EVENTS_STACKS[name].indexOf(handler);
if (idx !== -1) {
EVENTS_STACKS[name].splice(idx, 1);
}
return;
}
return originalRemoveEventListener.call(this, name, handler);
};
// Patch pushState / replaceState to emit route changes
const createPopStateEvent = (state, name) => {
const evt = new PopStateEvent('popstate', { state });
evt['trigger'] = name;
return evt;
};
const patchUpdateState = (updateState, name) => () => {
const before = window.location.href;
updateState.apply(this, arguments);
const after = window.location.href;
if (before !== after) {
handleUrlRoute(createPopStateEvent(window.history.state, name));
}
};
window.history.pushState = patchUpdateState(window.history.pushState, 'pushState');
window.history.replaceState = patchUpdateState(window.history.replaceState, 'replaceState');
};3.1.3 Loading
After matching a sub‑app via routing, we load its HTML and scripts.
async loadApp() {
// Find the active sub‑app
const shouldMountApp = this.Apps.filter(this.isActive);
const app = shouldMountApp[0];
const subapp = document.getElementById('submodule');
await fetchUrl(app.entry)
.then(text => { subapp.innerHTML = text; });
const res = await fetchScripts(subapp, app.entry);
if (res.length) {
execScript(res.reduce((t, c) => t + c, ''));
}
}Better practice – html‑entry library loads and processes HTML, JS, CSS of a micro‑app in four steps:
1. Request sub‑app entry HTML.
2. Strip html / head tags and handle static resources.
3. Process source maps, sandbox JS, locate entry script.
4. Retrieve sub‑app provider content.
export function provider({ dom, basename, globalData }) {
return {
render() {
ReactDOM.render(
<App basename={basename} globalData={globalData} />,
dom ? dom.querySelector('#root') : document.querySelector('#root')
);
},
destroy({ dom }) {
if (dom) {
ReactDOM.unmountComponentAtNode(dom);
}
}
};
}3.2 Sandbox
A sandbox isolates each sub‑application’s side effects (global variables, styles) to avoid conflicts.
Why we need sandbox?
Key is to control when the sandbox is activated and deactivated.
3.2.1 Snapshot Sandbox
Take a snapshot of the environment before switching, and restore it when returning.
// Switch to environment A
window.a = 2;
// Switch to environment B
window.a = 3;
// Switch back to A
console.log(a); // 2 class Sandbox {
private original;
private mutated;
sandBoxActive = () => {};
sandBoxDeactivate = () => {};
}
const sandbox = new Sandbox();
const code = "...";
sandbox.activate();
execScript(code);
sandbox.sandBoxDeactivate();3.2.2 VM Sandbox
Similar to Node’s vm module, it creates an isolated execution context using Proxy.
class SandBox {
execScript(code) {
const varBox = {};
const fakeWindow = new Proxy(window, {
get(target, key) { return varBox[key] || window[key]; },
set(target, key, value) { varBox[key] = value; return true; }
});
const fn = new Function('window', code);
fn(fakeWindow);
}
}
export default SandBox;
const sandbox = new Sandbox();
sandbox.execScript(code);
const sandbox2 = new Sandbox();
sandbox2.execScript(code2);3.2.3 CSS Sandbox
Webpack adds style tags via appendChild; we intercept this to namespace CSS.
Signed-in readers can open the original source through BestHub's protected redirect.
This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactand we will review it promptly.
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.
