Skip to content

    Frontend System Design

    Micro-Frontend Composition: Build-Time, Runtime, Iframes and the Edge

    Compare build-time, server, edge and browser composition of micro-frontends, including iframes and web components, and choose by isolation and release needs.

    By Published 13 min read

    • frontend-system-design
    • micro-frontends
    • composition
    • web-components
    Share

    Standard lessonintermediateFrontend System Design

    Before this: Monolith vs Micro-Frontends

    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.

    1. Build time: pieces are packages that the container compiles in.
    2. The server: a server assembles HTML fragments into one response.
    3. The edge: a CDN or proxy near the user assembles them.
    4. The browser, by loading separate documents or scripts: iframes, or scripts that expose a render function.
    5. 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

    Five places where independently built pieces can be joinedBuild time to Server (later); Server to Edge (later); Edge to Browser (later)Build time: pieces are packagesBuild timepieces arepackagesServer: HTML fragmentsServerHTML fragmentsEdge: CDN or proxyEdgeCDN or proxyBrowser: iframes, scripts, componentsBrowseriframes, scripts,componentslaterlaterlater

    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 sandbox attribute restricts the content; MDN strongly discourages combining allow-scripts and allow-same-origin for same-origin content, because the embedded document can then remove sandbox.
    • The title attribute 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

    sequenceDiagram accTitle: Client-side composition with a script and a render function accDescr: The container fetches a piece's manifest, adds its script tag, waits for it to load, and then calls the render function it exposes with a target element. participant C as Container participant H as Piece host participant P as Piece code C->>H: fetch asset manifest H-->>C: file names for the current build C->>H: add script tag for the piece H-->>C: script loads C->>P: call render(target, props) P-->>C: piece is on the page C->>P: call unmount(target) on navigation

    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

    flowchart TD accTitle: Choosing a micro-frontend composition approach accDescr: A decision flow. Content that needs no interactivity uses server or edge composition. If the piece must be fully isolated or is third-party, use an iframe. Otherwise use a web component or a federated module, choosing the loading mechanism separately. S["A piece to compose"] --> Q1{"Is it mostly content,
    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

    NOTE

    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

    Starting architecture: recommendations and basket are packages inside the page applicationRelated items pkg to Product page app (compiled in); Basket package to Product page app (compiled in); Product page app to One releaseRelated items pkg: team BRelated itemspkgteam BBasket package: team CBasket packageteam CProduct page app: team A, one buildProduct pageappteam A, one buildOne release: all three teams waitOne releaseall three teamswaitcompiled incompiled in

    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

    1. Define the contracts: the fragment URL, its cache headers and its fallback, and the element name, attributes and events.
    2. Ship the fragment path behind a flag with the budget set, and check page timing with the fragment fast, slow and down.
    3. Test the element in the real page, with the page's global styles loaded.
    4. Test both failure modes on purpose: block the fragment endpoint and the basket script.
    5. 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.

    In this course:

    Official documentation

    Ask about this article

    Get answers grounded in this post. AI-generated — based on this article, and may be imperfect.

    Scaled AI Weekly

    Enjoyed this? Get more like it every Monday.

    Real architecture decisions, LLMOps patterns that survive production, and engineering leadership advice — from 12+ years of building at enterprise scale. Free. No spam. Unsubscribe anytime.

    Get new articles by email

    Free. We email a confirmation link first, and you can unsubscribe from any email.

    Join engineers building production AI systems

    Comments