← Back to Micro Frontend
Lesson 3 · Micro Frontend Fundamentals

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.

Intermediate30 min

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.

Small dictionary
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:

parts/Catalog.jsx, parts/Cart.jsx, parts/Reviews.jsx
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.
monolith/main.jsx
// 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 /> </>, );
mf/cart.jsx (catalog.jsx and reviews.jsx are the same shape)
// 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 />); }
build.js
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.
sizes.sh
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 done
Output
dist-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.
Output - file names before and after
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.js

Result 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.
both.js
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(); })();
Output
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: none

Side 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 build

One entry, one bundle.

entryPoints: ['monolith/main.jsx']
Micro frontend build

One entry per piece.

entryPoints: ['mf/catalog.jsx', 'mf/cart.jsx', 'mf/reviews.jsx']
Content hash names

Changed content, changed name.

entryNames: '[name]-[hash]'
Gzipped size

Close 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.

Build it

“npm install react react-dom esbuild, copy the files, and run node build.js and bash sizes.sh.”

A bigger app

“Add 300 lines of real code to each part. How does the percentage difference in size change?”

Share React (a preview)

“Build the pieces with external: ["react", "react-dom"]. What fails in the browser - and why does the shell now need to provide React?”

Change the catalog

“Change one word in Catalog.jsx and rebuild. Which file names change in each version?”

What usually goes wrong

Thinking "monolith" means "bad"

A frontend monolith is simpler, smaller and easier to change across the app. It is the right default.

Ignoring duplicated libraries

Three pieces, three copies of React: 181 KB instead of 60 KB gzipped.

Measuring only the first visit

Also measure what changes after a release - that is where micro frontend wins.

Comparing only technology

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.

1.

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 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

How big was each version, gzipped?
Monolith 60,491 bytes; micro frontend 181,173 bytes.
Why is the micro frontend version about three times bigger?
Each of the three bundles contains its own copy of React.
Which files got new names after the one-word change?
Monolith: main-*.js (the only file). Micro frontend: only cart-*.js.
Did both versions work in the browser?
Yes - same text, Add worked, no errors.

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. 1.

    Why does a content hash in the file name matter for releases?

  2. 2.

    Why did three copies of React work on one page here?

  3. 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...