Vinicius Aguiar
Frontend

There's an Angular App Inside This Next.js Post

Sep 24, 2026 · 6 min read

I wanted to understand micro frontends by building one, not by reading about them. So I set myself a constraint: the experiment had to live inside a real page on this site, and the page itself had to be the proof. This post is that page.

This site is Next.js 16 with React 19. The panel below is not. It's an Angular 22 app, in its own repository, built and deployed on its own, and loaded at runtime from another domain. Apart from one URL, nothing in this site's build knows it exists.

Loading the Angular micro frontend…

No events from the micro frontend yet.

Try it: switch this site's theme or language, and the panel follows. Click its button, and the line under it, which belongs to the site and not to Angular, reports the event. Everything in the panel is read at runtime: the Angular version, the domain it came from, the commit it was built from, and how much it cost to download.

Three ways to compose a frontend

There isn't a single standard for micro frontends. There are three common ones, and they solve different problems.

  • Split by route. Each area of the product is a separate app, and a proxy or the edge sends each path to the right one. In Next.js this is called multi-zones. It's simple to run; the cost is a full page load every time you cross from one app to another.
  • Module Federation. The host loads modules from other deploys at runtime and shares dependencies with them. In the Angular world, Native Federation is the de facto option. It shines when every piece uses the same framework.
  • Web Components. The micro frontend is packaged as a custom element, and the host only has to render a tag. It's the one interface that React and Angular both understand natively.

Here the host is React and the micro frontend is Angular, so this is composition across frameworks. That's where Web Components are the usual answer, and it's exactly what Angular Elements is for.

The contract

The most useful way I found to think about a micro frontend is as a contract: everything one side knows about the other. Here, that's three things.

**Inputs are locale and theme.** This site sets them as properties on the element; a plain HTML page can set them as attributes, and Angular Elements maps both. The panel keeps its own dictionary for the five languages this site supports and its own colors for each theme. Nothing is inherited from the site's CSS.

Output is a DOM event. The panel doesn't know React exists. It dispatches a CustomEvent, and the site listens for it:

this.host.nativeElement.dispatchEvent(
  new CustomEvent("mfe:ping", {
    detail: { angularVersion: this.angularVersion, at: new Date().toISOString() },
    bubbles: true,
    composed: true,
  }),
)

Isolation is Shadow DOM. The panel's styles don't leak into the page, and the site's Tailwind doesn't leak into the panel.

On the Angular side, the whole micro frontend comes down to registering one tag:

import { provideZonelessChangeDetection } from "@angular/core"
import { createApplication } from "@angular/platform-browser"
import { createCustomElement } from "@angular/elements"
import { InspectorComponent } from "./app/inspector.component"

export const TAG = "mfe-inspector"

export async function registerInspector(): Promise<void> {
  if (customElements.get(TAG)) return
  const app = await createApplication({ providers: [provideZonelessChangeDetection()] })
  // Checked again: two evaluations of this file can both pass the first check.
  if (customElements.get(TAG)) return
  customElements.define(TAG, createCustomElement(InspectorComponent, { injector: app.injector }))
}

On the site side, loading it means importing a URL that the bundler must not touch, then waiting for the tag to be defined:

loadRemoteModule(async () => {
  // The bundler must not resolve this: the URL belongs to another deploy.
  await import(/* webpackIgnore: true */ /* turbopackIgnore: true */ src)
  await customElements.whenDefined(tag)
})

What it cost

These numbers were measured on the deployed file, not estimated:

  • The panel ships as a single file of 119 KB, or 42 KB compressed. For a panel with a heading, a short list and a button, that's the price of shipping a second framework to the page.
  • Over my connection, the median of five cold loads was 44 ms to download the file and 50 ms until the element was ready to use.
  • If the file doesn't load, the site shows a short notice in place of the panel in about a third of a second, and the rest of the post keeps working. For the case where the server never answers, there's an 8-second timeout.

The file is only requested when the panel gets close to the screen, so a reader who never scrolls this far never pays for Angular.

What the tutorials skip

Most of the work wasn't the component. It was the edges.

  • Trusting code from another domain. The site now runs JavaScript it didn't build. The post data only accepts a micro frontend from an allowlisted origin over HTTPS; anything else is discarded before the page renders.
  • One file, on purpose. The site imports a single URL. If the Angular build ever splits into chunks, that contract breaks silently, so the build fails loudly instead.
  • Registering twice. I expected leaving the page and coming back to run the module again and throw on a second definition of the tag. It doesn't: the browser evaluates a module once per URL. What does throw is the same file loaded from two URLs, and a simple check wasn't enough there either, because both evaluations pass it before either one registers. So the check runs again after Angular starts.
  • Measuring across origins. Without a Timing-Allow-Origin header, the browser reports 0 bytes for files from other domains, and the panel couldn't tell its own weight from a cached copy. A 304 revalidation has the same trap: only headers travel, so the panel says cache instead of claiming the module weighs 0.3 KB.
  • Deploying independently. The site loads the file at runtime, so a new deploy of the micro frontend reaches this post without rebuilding the site. I tested it by publishing a visible change to the panel alone. The short cache (60 seconds, plus up to 5 minutes serving the previous version while it revalidates) only limits how long a returning visitor can see the old one.

When it isn't worth it

For this site, a micro frontend is overkill, and that was the point: I wanted to know the cost before I ever needed to pay it. A second framework, a second pipeline, a contract to keep in sync, and code from another domain running on the page. With one team and one codebase, a well-organized monolith gets you almost all of the benefits for a fraction of that.

It starts to pay when the problem is people, not code: several teams, each with its own release cadence, stepping on each other in the same frontend. This site doesn't have that problem. Now I know what solving it costs.

The micro frontend's code is public: github.com/ViniAguiar1/mfe-angular-inspector.