Baike.dev
Log in
> 返回资讯列表
news_article.exe
📰

The browser only talks to one server — composing Marko, React, and Riot into one hotel page

2026年9月7日5 次浏览来源:Dev.to 阅读原文

You open a hotel page. It looks like one product: a search grid, a featured stay, local highlights, reviews, a sticky trip summary. Under the hood it is eight HTTP servers and three UI runtimes. That is the experiment behind HarborStay, a demo booking app I built to answer a stubborn question: Can independent teams ship independent UI, in independent frameworks, and still give the browser a single, paint-ready HTML page? The punchline: yes — if the shell never imports a component. It only fetches HTML. The one rule The browser never talks to a fragment. It talks to the composer on port . The composer owns routes, layout, and the booking flow. Everything else is a fragment server that returns a chunk of HTML. This is the opposite of the usual microfrontend story (Module Federation, shared...

You open a hotel page. It looks like one product: a search grid, a featured stay, local highlights, reviews, a sticky trip summary. Under the hood it is eight HTTP servers and three UI runtimes. That is the experiment behind HarborStay, a demo booking app I built to answer a stubborn question: Can independent teams ship independent UI, in independent frameworks, and still give the browser a single, paint-ready HTML page? The punchline: yes — if the shell never imports a component. It only fetches HTML. The one rule The browser never talks to a fragment. It talks to the composer on port . The composer owns routes, layout, and the booking flow. Everything else is a fragment server that returns a chunk of HTML. This is the opposite of the usual microfrontend story (Module Federation, shared React, a host that s widgets). HarborStay is HTML composition. The shell does not know whether a fragment was rendered by Marko, React, or a hand-rolled Riot string. It only knows a URL. That one constraint buys a lot: Fragment teams can pick a runtime without asking the shell. A fragment outage becomes a fallback box, not a blank page. You can deploy search without redeploying reviews. It also forces honesty. If two fragments need to share a Redux store, the architecture is already leaking. What the user actually sees HarborStay models a small premium catalog: Harbor View Lodge in Lisbon, Copper Hill Suites in Seville, Sunset Terrace House in Barcelona, Moss & Lantern in Copenhagen. Routes the composer owns: Route What you get and Streaming stays page Neighborhood explorer + day plan Confirmation (I got for Moss & Lantern) On the page is not “a React tree.” It is a layout of slots: Click Luxury on the search chips and the grid collapses to Harbor View Lodge and Moss & Lantern. The reviews below do not re-fetch. They never heard about the filter. That is not a bug — that is the point. Island rule: each fragment owns its own interactivity. Search filters are Marko state. Highlight selection is React state. Experience categories are Riot state. The composer does not have a global store. A tiny protocol instead of a shared SDK Every fragment speaks . And a few provenance headers: The composer reads the HTML body. Asset URLs are informational. In this demo, CSS and JS are served from a tiny CDN app on that copies fragment assets on startup. That split is the real-world shape: SSR lives with the team that owns the UI. Bytes live on a CDN. Assets belong to the fragment that created them, not to the shell: If search wants a new filter chip, reviews does not rebuild. Three runtimes, one HTML contract This is the part people assume is impossible. Runtime Fragments Server render Client Marko navigation, search, details, reviews, recommendations → Marko DOM runtime mounts into a root React 19 local highlights Riot experiences discovery + itinerary hand-written after clearing the SSR root The composer does not care. All three return strings. Why SSR in a microfrontend at all? The first paint does not wait for JavaScript. HTML is the only lingua franca three frameworks share. If fails, the user still sees the cards. That last one is Islands architecture in the small: static HTML with pockets of JS, not a SPA that hydrates the entire document. How data crosses the SSR/client gap Every interactive fragment embeds props as JSON, not attributes: The client bundle reads that script and mounts. Before embedding, fragment servers escape as so a hotel description cannot break out of the script tag. looks simpler. It is worse: large JSON in attributes bloats the DOM, needs ugly escaping, and is visible to everything that walks the tree. Hydration is not one word Walking the live app made this concrete. Marko search hydrates into . Luxury / Value / Top rated re-render the grid without a navigation. The client template must match the server template or you get a flash. React highlights hydrates with . On Moss & Lantern you get Garden loop, Sauna ritual, Harbor market. Click a card and only that island updates. Riot cannot hydrate the way React does. There is no reconcile-existing-DOM API in this setup, so the client wipes the SSR HTML and remounts: You still get a fast first paint. You also get a tiny risk of visual flash if the Riot template and drift. Keep class names, element types, and copy identical. On I clicked Food: three cards became one tasting session. I clicked Day 2: breakfast / walk / dinner became scenic run / museum / sunset lounge. Two Riot islands, two state machines, zero shared store. Streaming: do not wait for the slowest fragment The stays page does not then send one blob. is an . It yields the and shell immediately, then races fragment promises and flushes HTML as each one finishes. Fastify wraps that in . The response header is a breadcrumb that this is a streamed document. The demo even fakes latency so you can see streaming: In production you delete those delays. Keep the race. First Contentful Paint becomes “when the shell arrives,” not “when reviews finally woke up.” Skeletons () hold layout so later chunks do not shove the page around. If a fragment is restarting, catches the error and yields: Independent deploys stop being theoretical. JavaScript budget is a route problem A composed page can accidentally ship four frameworks to every visitor. HarborStay avoids that with page splits, not heroics. runs composer + CDN + stays fragments. runs composer + CDN + the two Riot apps. The stays page builder never emits . The experiences page never emits . That is the cheapest performance win in the repo. Intersection Observer hydration, click-to-hydrate, and are next — the docs already sketch them — but route-level omission beats all of them. State: URL, local, or an event — pick one Shared mutable global state is how microfrontends become a distributed monolith. HarborStay’s rules: Ephemeral UI state stays in the island. Filter chips, selected highlight, active day. Shareable state lives in the URL. is the source of truth for “which stay.” Full page reload is intentional in this demo. Cross-fragment talk is an event, not a store import. Do not do this: Two IIFE bundles that “import the same store” get two closures. They will not share state. They will gaslight you. is the right tool only when tabs or iframes must stay in sync. is for values that must survive a round trip. Neither belongs in the first version. How this would ship Each fragment is a Node HTTP server. DNS for users points at the composer. Fragments live on internal hostnames. Static assets go to a real CDN. Rolling deploy order: Publish content-hashed assets. Roll fragment servers (any order — fallbacks cover restarts). Roll the composer last. Turborepo is the monorepo glue: fans out with persistent tasks, and only rebuilds what changed. This is a demo, not a production platform. The goal is a shape you can reason about: one contract, many runtimes, progressive HTML. What I would do next The interesting unfinished work is not “add another framework.” It is: Soft navigation — + swap only the fragments that changed. True streaming inside fragments — Marko , React . Riot hydrate() when the API is stable, so we stop wiping SSR DOM. Edge composer — the generator already looks like a Web Streams mental model. SWR cache of fragment HTML at the composer for anonymous hotel pages. Run it Then open . If you want a mental model to steal, steal this one: Compose HTML, not components. Hydrate islands, not pages. Keep state with the team that owns the UI. Let the slowest fragment arrive last without blocking the first paint. The user still sees one hotel page. The architecture is allowed to be many rooms behind one door. GITHUB: https://github.com/piyushchauhan2011/marko-mfe

> 分享:
Baike.dev

baike.dev helps you discover great languages, frameworks, databases, DevOps and cloud-native tools.

Quick links

About

Contribute

Found a great developer tool? Share it with the community.

Submit a tool
© 2026 baike.dev Developer EncyclopediaUpdated daily · Discover great developer tools