Monolith vs Micro Frontend
The same small React app, built both ways with esbuild and measured. The monolith ships one copy of React (60 KB gzipped); the micro frontend version ships three (181 KB). But after a one-word change in the cart, the monolith’s whole bundle gets a new name, while in the micro frontend only the cart file changes. Both run in a real browser.
What you will be able to do
- Build the same app as a monolith and as micro frontends
- Measure the cost of duplicated libraries
- See what changes for users after a small release
- List what a frontend monolith does well
- Compare both on the points that matter for a decision
The idea, in plain English
A frontend monolith is one app, built into one bundle (or a few chunks) and released as one thing. That is not a bad word: it is simple, fast to load, easy to test and to refactor across the whole app. Most frontends should be monoliths.
Micro frontends trade some of that for independent releases. Each piece is built on its own - so by default each piece brings its own copy of every library it uses. The user downloads more, and the teams must agree on how pieces talk. In return, a change in one piece touches only that piece.
We build one small React app both ways with esbuild, then measure: bytes, files, and which files change after a one-word edit. React 19.2.3, esbuild 0.28.2, Chromium through Playwright.
Worked example: Catalog, cart and reviews in React - one bundle versus three separately built pieces, compared on size, files and what changes on release.
Words you will see in this lesson
A few build words.
BundleOne JavaScript file that contains your code and the libraries it uses.BundlerA tool that makes bundles - esbuild, Vite, webpack.Content hashA short code in a file name, made from its content: main-YMHK336O.js. New content, new name.gzipCompression servers use when sending files; the gzipped size is closer to what users download.Shared dependencyA library several pieces use - like React.An everyday example: a family trip
- One family in one car: one map, one first-aid kit, one bag of snacks. Cheap - but everyone leaves when the slowest person is ready.
- Three friends in three cars: each car needs its own map and kit - three times the luggage. But each car leaves when it is ready, and a flat tyre stops only one car.
- The duplicated luggage is the duplicated React. The independent cars are the independent releases.
Example 1 - one app, three parts
Three small React components. Cart has state and a button, so we can check it really works:
export function Catalog() {
return <p>Catalog: 3 books</p>;
}
import { useState } from 'react';
export function Cart() {
const [items, setItems] = useState(0);
return <p>Cart: {items} items <button onClick={() => setItems(items + 1)}>Add</button></p>;
}
export function Reviews() {
return <p>Reviews: 12 reviews</p>;
}Example 2 - build it both ways
- monolith/main.jsx imports all three and renders them into one React root - one entry point.
- mf/cart.jsx (and catalog, reviews) imports ONE part and exports mount(element) - the contract from Lesson 1. Each is its own entry point.
- build.js runs esbuild twice with the same settings: minified, production mode, and [name]-[hash] file names.
- Every micro frontend entry imports react-dom - so esbuild puts a full copy of React into each one.
// ONE app: all three parts in one bundle, one copy of React
import { createRoot } from 'react-dom/client';
import { Cart } from '../parts/Cart.jsx';
import { Catalog } from '../parts/Catalog.jsx';
import { Reviews } from '../parts/Reviews.jsx';
createRoot(document.getElementById('app')).render(
<>
<Catalog />
<Cart />
<Reviews />
</>,
);// A separate build for team cart - it brings its OWN copy of React
import { createRoot } from 'react-dom/client';
import { Cart } from '../parts/Cart.jsx';
export function mount(element) {
createRoot(element).render(<Cart />);
}const { buildSync } = require('esbuild');
const common = {
bundle: true, minify: true, format: 'esm', jsx: 'automatic',
entryNames: '[name]-[hash]', // a new file name when the content changes
nodePaths: [process.env.REACT_MODULES],
define: { 'process.env.NODE_ENV': '"production"' },
logLevel: 'error',
};
buildSync({ ...common, entryPoints: ['monolith/main.jsx'], outdir: 'dist-monolith' });
buildSync({ ...common, entryPoints: ['mf/catalog.jsx', 'mf/cart.jsx', 'mf/reviews.jsx'], outdir: 'dist-mf' });Result 1 - size: three copies of React
sizes.sh lists every bundle with its size and gzipped size:
- Monolith: one file, 194,434 bytes (60,491 gzipped).
- Micro frontend: three files of about 194,000 bytes each - almost all of it React. Total 582,396 bytes (181,173 gzipped): three times as much.
- Our components are tiny, so React is nearly everything here. In a real app the gap is smaller in percent, but every duplicated library adds up.
- Three copies of React also mean three separate React instances on one page. That worked here, because the pieces do not share components or hooks - Module 3 (Lesson 21) and Lesson 26 show how to share one copy.
for dir in dist-monolith dist-mf; do
echo "$dir:"
total=0; gz=0
for f in $dir/*.js; do
size=$(wc -c < $f | tr -d ' '); zipped=$(gzip -9 -c $f | wc -c | tr -d ' ')
printf " %-24s %7s bytes (%6s gzipped)\n" $(basename $f) $size $zipped
total=$((total + size)); gz=$((gz + zipped))
done
printf " %-24s %7s bytes (%6s gzipped)\n" "TOTAL" $total $gz
donedist-monolith:
main-YMHK336O.js 194434 bytes ( 60491 gzipped)
TOTAL 194434 bytes ( 60491 gzipped)
dist-mf:
cart-N55QEYUG.js 194199 bytes ( 60439 gzipped)
catalog-NLP3GE4H.js 194097 bytes ( 60368 gzipped)
reviews-B4K7FU4U.js 194100 bytes ( 60366 gzipped)
TOTAL 582396 bytes (181173 gzipped)Result 2 - a small release
We changed one word in Cart.jsx ("items" to "books") and built again:
- Monolith: main-YMHK336O.js became main-ZNLNBYE4.js. Every user downloads all 194 KB again - for one word - and every team’s code was released with it.
- Micro frontend: only cart-N55QEYUG.js became cart-LY3XHFXC.js. catalog and reviews kept their names, so browsers keep them from the cache, and those teams were not involved.
- This is the release side of the trade: small, separate changes - paid for with the bigger first download.
before the change:
dist-mf:
cart-N55QEYUG.js
catalog-NLP3GE4H.js
reviews-B4K7FU4U.js
dist-monolith:
main-YMHK336O.js
after changing one word in Cart.jsx:
dist-mf:
cart-LY3XHFXC.js
catalog-NLP3GE4H.js
reviews-B4K7FU4U.js
dist-monolith:
main-ZNLNBYE4.jsResult 3 - both work in a browser
We served both builds and opened them in Chromium with Playwright, then clicked Add once:
- The monolith loaded 1 JS file, the micro frontend 3.
- Both pages showed the same text, and the cart count went to 1 in both. No errors.
- To the user, the two are the same page. The difference is in how they are built, released - and how much they download.
const { chromium } = require('playwright');
(async () => {
const b = await chromium.launch();
for (const [name, url] of [['monolith', 'http://localhost:4201/'], ['micro frontend', 'http://localhost:4202/']]) {
const p = await b.newPage();
const js = [];
p.on('response', async (r) => { if (r.url().endsWith('.js')) js.push(r.url().split('/').pop()); });
const errors = []; p.on('pageerror', (e) => errors.push(e.message));
await p.goto(url, { waitUntil: 'networkidle' });
await p.click('button');
const text = (await p.innerText('body')).replace(/\n+/g, ' | ');
console.log(`${name}: ${js.length} JS files (${js.join(', ')})`);
console.log(` page after one click on Add: ${text}`);
console.log(` errors: ${errors.length ? errors.join('; ') : 'none'}`);
await p.close();
}
await b.close();
})();monolith: 1 JS files (main-ZNLNBYE4.js)
page after one click on Add: Catalog: 3 books | Cart: 1 books Add | Reviews: 12 reviews
errors: none
micro frontend: 3 JS files (catalog-NLP3GE4H.js, cart-LY3XHFXC.js, reviews-B4K7FU4U.js)
page after one click on Add: Catalog: 3 books | Cart: 1 books Add | Reviews: 12 reviews
errors: noneSide by side
What we measured, plus what you will meet later in the course:
- First download - monolith: smaller (one copy of each library). Micro frontend: bigger, until shared dependencies are solved.
- Small release - monolith: the whole app is rebuilt and re-downloaded. Micro frontend: only the changed piece.
- Who releases - monolith: everyone together. Micro frontend: each team alone.
- Refactoring across parts - monolith: one change, one commit. Micro frontend: several teams, several releases, and a contract to keep stable.
- Testing the whole page - monolith: one test suite. Micro frontend: each piece, plus the pieces together.
- Consistent look - monolith: one place. Micro frontend: needs shared components that every piece uses (Lesson 71).
- If you have one or two teams, the monolith wins almost every row. Lessons 6 and 7 turn this into a decision.
Monolith vs micro frontend at a glance
Monolith buildOne entry, one bundle.
entryPoints: ['monolith/main.jsx']
Micro frontend buildOne entry per piece.
entryPoints: ['mf/catalog.jsx', 'mf/cart.jsx', 'mf/reviews.jsx']
Content hash namesChanged content, changed name.
entryNames: '[name]-[hash]'
Gzipped sizeClose to the real download.
gzip -9 -c file.js | wc -c
Try it yourself
The code does not change. Swap the content string and the program does something else entirely.
“npm install react react-dom esbuild, copy the files, and run node build.js and bash sizes.sh.”
“Add 300 lines of real code to each part. How does the percentage difference in size change?”
“Build the pieces with external: ["react", "react-dom"]. What fails in the browser - and why does the shell now need to provide React?”
“Change one word in Catalog.jsx and rebuild. Which file names change in each version?”
What usually goes wrong
A frontend monolith is simpler, smaller and easier to change across the app. It is the right default.
Three pieces, three copies of React: 181 KB instead of 60 KB gzipped.
Also measure what changes after a release - that is where micro frontend wins.
The deciding points are team count and release friction, not bundle tools.
Practice
Write these yourself before opening anything. Getting them wrong first is most of how this sticks.
Add a fourth part, Account.jsx, to both versions and rebuild. Record the total gzipped size for 3 and 4 parts in both versions, and estimate the size for 10 parts. Then write three sentences: at this size, who would notice the difference - a user on a fast laptop, or one on a slow mobile network?
Show hintHide hint
Each new micro frontend entry adds roughly one more copy of React (about 60 KB gzipped); the monolith adds only the new component.
Key points
- A frontend monolith is one build and one release - simple, small, easy to refactor.
- Separately built pieces each bring their own libraries unless you share them.
- Measured: 60 KB vs 181 KB gzipped for the same three parts.
- After a small change, the monolith is re-downloaded whole; a micro frontend only the changed piece.
- To the user, both can be the same page.
- With one or two teams, the monolith usually wins.
Quick check before you move on
Interview questions
What are the main costs of micro frontends compared with a monolith?
Larger payloads from duplicated dependencies unless shared, runtime integration complexity, cross-team contracts, harder end-to-end testing and consistency, and more infrastructure (pipelines, hosting, monitoring per piece).
How do you avoid shipping multiple copies of React in micro frontends?
Share dependencies at runtime - for example Module Federation shared singletons or import maps with externals - and agree on compatible version ranges; or integrate at build time when independent deploys are not required.
Quiz
- 1.
Why does a content hash in the file name matter for releases?
- 2.
Why did three copies of React work on one page here?
- 3.
Name three things a frontend monolith does better.
Comments
Sign in to leave a comment. Your name and photo come from Google; nothing else is shared.
Loading comments...