What you'll learn: By the end of this lesson you will be able to name the points at which micro-frontends can be joined into one page (build time, the server, the edge and the browser), say what each approach isolates and what it couples, and choose an approach for a page from its need for interactivity and independence.
A newspaper front page is made from stories written at different desks. The editors could assemble it at the printing press, at the regional depot before the vans leave, at the newsagent's counter, or leave the pieces for the reader to put together at the breakfast table. Each choice changes who can change a story late and what happens when one desk misses its deadline.
Micro-frontend composition is the same question: when and where do separately built pieces become one page? The previous lesson, Monolith vs Micro-Frontends, covered whether to split. This one covers how the pieces are joined. The running example is the same grocery storefront: a content-heavy product page, a related-items panel owned by one team, and a basket widget owned by another.
The core idea
The composition point decides what is isolated, what is coupled, and what happens when one piece fails. There are five common points, and each moves the "join" to a different place.
- Build time: pieces are packages that the container compiles in.
- The server: a server assembles HTML fragments into one response.
- The edge: a CDN or proxy near the user assembles them.
- The browser, by loading separate documents or scripts: iframes, or scripts that expose a render function.
- The browser, by browser or tool standards: web components, or a bundler feature such as Module Federation.
The main references are Cam Jackson's Micro Frontends on martinfowler.com and Michael Geers's micro-frontends.org, plus MDN, a W3C Note and vendor documentation for the specifics.
Figure 1. Where the join happens
How to read this: left to right is later in the life of a page. The later the join, the later a team's change can reach users without anyone else rebuilding, and the more the join happens on the user's device.
Build-time composition
In build-time integration each piece is published as a package and the container includes it as a library dependency. It is the simplest to understand and it reuses what teams already know.
Jackson's article advises against it for micro-frontends: it forces "a lockstep release process", because every piece must be recompiled and released to change any part of the product. It shares code well and gives no independent deployment, so use it for what changes rarely, such as design tokens, and not for pieces teams want to ship on their own.
Server-side composition
A server builds the page from HTML fragments, each a request to another service that returns HTML. In Jackson's example they are joined with server-side includes and each can be deployed independently. Geers calls this web-server-level integration and says it suits content-heavy sites near the "documents" end of the spectrum. The browser needs no framework to show the result. The cost is that the assembler waits for its fragments, so its response is only as fast and reliable as the slowest one.
The W3C Note on Edge Side Includes (ESI), dated 4 August 2001 and submitted for discussion only, defines a markup include element with src, optional alt and optional onerror attributes: if src cannot be fetched the processor uses alt, and if neither can be fetched and onerror="continue" is set it silently removes the element. That is a small, clear statement of the policy every assembler needs: what is shown when a fragment is missing.
Minimal example: a page assembler with fallbacks
The code below is a toy, not an ESI implementation: it copies only the src, alt and onerror="continue" behaviour described in the Note, against a fake network. It was run with Node.js 26.12.
const INCLUDE = /<esi:include\s+([^>]*?)\/>/g;
function attr(tag, name) {
const m = new RegExp(`${name}="([^"]*)"`).exec(tag);
return m ? m[1] : undefined;
}
async function assemble(template, fetchFragment) {
const matches = [...template.matchAll(INCLUDE)];
let out = template;
for (const m of matches) {
const src = attr(m[1], "src");
const alt = attr(m[1], "alt");
const onerror = attr(m[1], "onerror");
let html = await fetchFragment(src);
if (html === null && alt) html = await fetchFragment(alt);
if (html === null) {
if (onerror !== "continue") throw new Error(`cannot include ${src}`);
html = "";
}
out = out.replace(m[0], html);
}
return out;
}
// Fake network: the cart fragment is down, its backup works, the promo fragment is down with no backup.
const fragments = {
"/f/header": "<header>Shop</header>",
"/f/cart": null,
"/f/cart-static": "<aside>Cart unavailable</aside>",
"/f/promo": null,
};
const fetchFragment = async (url) => fragments[url] ?? null;
const page = `<body>
<esi:include src="/f/header" />
<esi:include src="/f/cart" alt="/f/cart-static" />
<esi:include src="/f/promo" onerror="continue" />
</body>`;
console.log(await assemble(page, fetchFragment));
try {
await assemble('<esi:include src="/f/promo" />', fetchFragment);
} catch (e) {
console.log("strict include failed:", e.message);
}
Output from that run:
<body>
<header>Shop</header>
<aside>Cart unavailable</aside>
</body>
strict include failed: cannot include /f/promo
The cart fragment failed, so its alt was used. The promo fragment failed with no backup and onerror="continue" removed it. Without that attribute this toy fails the whole page, which is a choice of the toy, not a statement about the Note. The design decision is real: a fragment that is optional must say so.
Edge-side composition
Edge-side composition is server-side composition done by an intermediary near the user. The ESI Note describes the processors as HTTP surrogates, such as reverse proxies and content delivery networks, and says that because parts of a page can be cached separately, a surrogate can serve more content from cache. Its stated aims are better perceived performance, less origin processing and better availability.
The mechanism differs by provider. Cloudflare's documentation, as one example, describes HTMLRewriter, a Workers API that transforms HTML as a stream and whose handlers may call fetch or the cache. It also states limits: if a handler throws, parsing stops and the client may receive a truncated response. Which providers support ESI itself was not verified here, so check yours. Edge Rendering covers what runs well at the edge, and HTTP Caching covers making fragments cacheable.
The cost is an assembly step you may not control, and fragments that need cache keys and invalidation.
Client-side composition
Client-side composition joins pieces in the browser, which suits interactive pieces. There are four common ways.
Iframes
An <iframe> embeds another document in its own nested browsing context, with its own document. Jackson's article calls this the easiest route to isolating styles and globals, and also "less flexible": it complicates routing, history and deep linking and presents extra challenges for responsive layout. MDN adds practical notes:
- Each iframe costs extra memory and compute, so check pages that use many.
- The
sandboxattribute restricts the content; MDN strongly discourages combiningallow-scriptsandallow-same-originfor same-origin content, because the embedded document can then removesandbox. - The
titleattribute should describe the content for assistive technology.
Cross-origin frames follow the browser's same-origin rules, which The Same-Origin Policy and CORS explains.
Scripts that expose a render function
In Jackson's demo each piece is loaded with a script tag and exposes a global function, such as render, that the container calls with a target element. The article calls this the most flexible option and the team's default, with independent deployment and full control over integration, and says the function's signature is the key contract. The price is that pieces share the page's global scope, so styling and naming conflicts are possible, and the teams define the contract themselves.
Figure 2. Loading a piece with a render function
How to read this: the container learns the current file names, loads the script and calls an agreed function. The agreed function names and arguments are the contract. If the piece changes them without telling the container, the page breaks.
Web components
Web components are a set of browser technologies for creating reusable custom elements whose functionality is encapsulated away from the rest of the code. MDN names three: custom elements (JavaScript APIs to define an element and its behaviour), shadow DOM (an encapsulated tree attached to an element, keeping its styles and ids from colliding with the rest of the document) and HTML templates (<template> and <slot>).
For composition, the custom element is the contract: the container uses a tag such as <shop-checkout> and sets attributes. The name must contain a hyphen, and the specification recommends setup in connectedCallback() rather than the constructor, with cleanup in disconnectedCallback(). Geers describes custom elements as the main browser-level integration technique, a way to hide implementation details behind a neutral interface.
class CheckoutElement extends HTMLElement {
connectedCallback() {
const root = this.shadowRoot ?? this.attachShadow({ mode: "open" });
const p = document.createElement("p");
p.textContent = `Checkout for basket ${this.getAttribute("basket-id") ?? "none"}`;
root.replaceChildren(p);
}
disconnectedCallback() {
// release timers, subscriptions and listeners here
}
}
customElements.define("shop-checkout", CheckoutElement);
This snippet uses only APIs documented by MDN, but it was not run, because it needs a browser. Page CSS does not reach into the shadow tree and its styles do not leak out, which addresses the global stylesheet problem for a piece's internals; how inherited styles cross the boundary is worth checking in the specification. MDN warns that a closed shadow root is not a security boundary. A web component also does not decide how a piece loads its code, shares dependencies or talks to other pieces.
Module Federation
Module Federation is a bundler feature, described in its documentation as an architectural pattern for splitting JavaScript applications into parts, in which an application loads modules that other applications expose. It joins pieces in the browser at run time and negotiates shared dependencies. The next lesson, Module Federation in React, covers its configuration.
Choosing: a comparison
| Approach | Where the join happens | Isolation | Independent deploy | Main cost |
|---|---|---|---|---|
| Build time | In the container's build | None beyond module scope | No: lockstep release | Every change rebuilds and releases the whole |
| Server-side | On an origin server | Strong: separate services | Yes, per fragment | Page waits on its slowest fragment |
| Edge-side | At a CDN or proxy | Strong | Yes, plus per-fragment caching | Vendor-specific assembly and its limits |
| Iframe | In the browser, nested document | Strongest in the browser | Yes | Routing, history, sizing, extra memory per frame |
| Script with render function | In the browser, shared page | Weak unless you add rules | Yes | Global scope, hand-made contract |
| Web component | In the browser, custom element | Style isolation via shadow DOM | Yes | Does not solve loading or sharing dependencies |
| Module Federation | In the browser, via bundler runtime | Shared scope by design | Yes | Shared dependency and version coordination (next lesson) |
The approaches also combine: server or edge composition for the page's static areas, with web components or federated modules for the interactive islands.
Figure 3. Picking an approach
with little interaction?"} Q1 -->|"yes"| SE["Server or edge fragments
with a fallback each"] Q1 -->|"no"| Q2{"Must it be fully isolated
or is it from another origin?"} Q2 -->|"yes"| IF["Iframe with sandbox,
title and sized layout"] Q2 -->|"no"| Q3{"Must it share dependencies
with the container?"} Q3 -->|"yes"| MF["Federated module
(next lesson)"] Q3 -->|"no"| WC["Web component or
render-function script"]
How to read this: the questions go from the cheapest, most isolated approach to the most coupled. Answer them per piece, not per product: one page can use three approaches.
In practice: content pages with a recommendations panel and a basket widget
This is a realistic scenario, not a case history. It is a composite built from common production patterns, describes no specific company, and every number is an illustrative assumption.
The situation
The storefront's product pages are rendered by one team. A second team owns a related-items panel, and a third owns a basket widget that updates without a page load. Both are npm packages built into the page team's application, so a change to either needs a rebuild and release of the whole page. The related-items team wants to ship daily, and the basket team wants interactivity that does not wait for the page's release.
Starting architecture
Figure 4. Everything compiled into one application
How to read this: two packages are compiled into one application, so each team's change waits for the same release.
Options considered
| Option | What it fixes | What it costs | Risk |
|---|---|---|---|
| Keep build-time packages | Nothing new | Nothing | Lockstep releases continue |
| Server fragments for recommendations, web component for the basket | Both teams release alone | A fragment endpoint, a time budget, a custom element contract | Slow fragment, contract drift |
| Module Federation for both | Shared dependencies | Version coordination, bundler lock-in | Covered in the next lesson |
The decision
| Field | Decision record |
|---|---|
| Context | Two teams need to release independently of the page team. Recommendations are content with little interaction. The basket is interactive. |
| Decision | Recommendations become a server-composed fragment with a time budget and a cached fallback. The basket becomes a custom element with attributes and events as its contract. |
| Consequences, good | Each team ships alone. The page renders HTML with no extra framework for the panel. |
| Consequences, bad | The page team owns an assembler with timeouts. The basket needs its own bundle and a documented element contract. |
| Revisit when | The basket needs to share a large dependency with the page, or a third interactive piece appears. Then evaluate Module Federation. |
Rollout and verification
- Define the contracts: the fragment URL, its cache headers and its fallback, and the element name, attributes and events.
- Ship the fragment path behind a flag with the budget set, and check page timing with the fragment fast, slow and down.
- Test the element in the real page, with the page's global styles loaded.
- Test both failure modes on purpose: block the fragment endpoint and the basket script.
- Watch real-user load metrics before and after.
The 2 a.m. failure
At night the related-items service slows down. Product pages take much longer to appear and then time out, although the panel's own dashboards look healthy because the service still answers, only slowly. The page inherits the slowest fragment, and a fragment that is down takes the page with it.
The cause is that the assembler's time budget was set on the connection and not on the whole fragment request, so a slow response never hit it.
First three checks: (1) compare page response time with the fragment's response time in the assembler's logs; (2) confirm which timeout applies and what happens when it fires; (3) check that a cached fallback exists. Mitigation: turn the fragment off with its flag. Follow-up: enforce a total time budget per fragment (an illustrative 300 ms, tuned from measurements), serve the fallback on expiry, mark optional fragments with onerror="continue" or its equivalent, alert on fragment timeouts, and add a failure test to the pipeline.
Architect's notes
- Choose per piece, not per product. Content can be a fragment, an interactive widget a custom element, a third-party tool an iframe.
- Write the failure policy first. Decide what the page shows if a piece is slow, missing or broken.
- Keep the contract small and versioned. Tag names, attributes, events and fragment URLs are public API.
Common mistakes
- No fallback for a fragment. The page inherits the slowest piece's outage.
- Iframes for everything. Isolation is strong but routing, history and sizing become hard.
- Assuming shadow DOM isolates everything. Page CSS does not reach in, but code loading, shared dependencies and communication are separate matters.
- Assuming a vendor supports a feature. Check what your CDN or edge platform really supports.
Key takeaways
- Composition is the choice of when and where pieces become one page: build time, server, edge, or browser.
- Build-time integration forces lockstep releases and is a poor fit for independent deployment.
- Server and edge composition suit content, and every fragment needs a time budget and a fallback.
- Iframes give the strongest isolation and the most awkward routing, history and sizing.
- A render-function script is flexible, but the contract is hand-made and the page scope is shared.
- Web components give a stable element contract and style isolation, but do not decide how code loads.
- One page can use more than one approach, and Module Federation (next lesson) is one more option.
Practice exercises
Exercise 1 — Starter (30 min): For three pieces of a page you know (for example a header, a basket and a recommendations panel), choose a composition approach for each and write one sentence on the failure behaviour you want.
Exercise 2 — Intermediate (2 hours): Extend the toy assembler so each fragment has a time budget, using a fake fetch that resolves after a chosen delay. Show a case where the budget fires and the alt content is used.
Exercise 3 — Advanced (half day): Wrap a small widget as a custom element with a shadow root and test it in a page with a global button style. Record which of its styles the rule changes and which it does not.
Related reading
In this course:
- Monolith vs Micro-Frontends, whether to split at all.
- Module Federation in React, run-time composition through the bundler.
- Edge Rendering, what runs well at the edge.
- HTTP Caching, making fragments cacheable.
- The Same-Origin Policy and CORS, what limits a cross-origin frame.
Official documentation
- Micro Frontends, by Cam Jackson (martinfowler.com), the integration approaches.
- Micro Frontends, by Michael Geers (micro-frontends.org), web-server-level and browser-level integration.
- W3C Note: Edge Side Includes (ESI) language, the
includeelement and its fallbacks. - MDN: Web components, Using custom elements and Using shadow DOM.
- MDN: The iframe element.
- Cloudflare Workers: HTMLRewriter, one provider's streaming HTML transform.