The Idea
A big app is rarely built, shipped, or owned by one team. Micro-frontends split a site into independent pieces — each with its own codebase, release schedule, and owner — that come together in the browser. They’re the front-end cousin of microservices.
There are two ways to bring a remote piece into a host page:
- Module federation ships code: the host downloads the remote’s JavaScript and runs it itself, so the host owns the render.
- App federation — Marko’s approach — ships rendered HTML: the remote app renders its own piece (fetching its own data, running its own logic) and the host simply embeds the result. The remote owns the render; the host just knows a URL.
Marko does this with the <micro-frame> tag. It behaves like an <iframe> — “show me
what lives at this URL” — but instead of an isolated frame, the fetched HTML is streamed
straight into the page. This embedding of one document’s output inside another is called
transclusion:
This workspace has two separate apps — look at the file tree: a host/ app on port
3000 and a remote/ app on port 3001, each with its own package.json. They can even be
on different Marko versions (this host is on Marko 5 so it can use the @micro-frame/marko
tags; the remote is a plain Marko 6 app). Each keeps its own Marko runtime, kept apart by
a runtimeId (set in the remote’s vite.config), so the two don’t clash once the remote’s
HTML lands inside the host page.
The remote notice is a real, interactive component — try dismissing it with the ×. Its
JavaScript is served by the remote, but the browser can’t reach the remote’s port directly, so
the host forwards those requests through itself (the /remote-assets/ route in host/). That’s
the point of app federation: the remote is a fully independent app — its own render, data, and
interactivity — and the host just embeds the result and relays what the browser needs. Open
both previews: the host page already embeds the remote’s notice with <micro-frame>, and
the remote serves that same notice on its own at /fragment. Unlike an iframe, the embedded
notice is just part of the host’s HTML — no box, no separate scrollbar, and it’s visible to search
engines and screen readers.
Seeing the runtime isolation
How do two Marko runtimes share one page without stepping on each other? The runtimeId.
The remote is built with runtimeId: 'mr', so every marker and every piece of hydration data
it emits is namespaced with mr — no other app’s runtime uses that key, so nothing collides.
Open the Remote app preview (port 3001), then use your browser’s View Source to see the
real bytes. Alongside the notice’s HTML you’ll find Marko’s component-boundary comments and a
small hydration script, all carrying the mr id — roughly like this (simplified):
<!--mr ...--> <!-- component boundary, namespaced by runtimeId --><div ...>…the notice…</div><script>$mr_C = window.$mr_C || []; /* the remote's hydration registry, keyed by "mr" */ $mr_C.push(/* scope + state for the × button */)</script>When the host embeds this, its own runtime lives under a different id, so the two sets of
markers and registries never overlap. That’s what lets you drop as many remotes as you like
onto one page — each stays sandboxed by its own runtimeId.
In the next lesson you’ll wire the embed up yourself.
- Installing the host app
- Installing the remote app
- Building the remote, then starting both apps