How Cloudflare’s Fragments Architecture Enables Ultra-Fast Micro‑Frontend Rendering
This article explains Cloudflare’s new Fragments Architecture for micro‑frontends, detailing how parallel, streaming responses from Workers eliminate common bottlenecks, provide eager interactivity, improve security, and achieve extreme first‑paint performance through innovative fragment hosting and communication mechanisms.
Editor’s note: the author, a former Ant Group frontend engineer, introduces Cloudflare’s new micro‑frontend solution and the extreme first‑paint optimization behind it.
Problems with Existing Client‑Side Micro‑Frontend Approaches
Module Federation cannot tree‑shake shared libraries during build.
Shared libraries cause implicit coupling and make version upgrades painful (reason qiankun avoids externals).
Waterfall requests require the root app to start before micro‑apps can fetch, causing first‑paint delays.
Hydration delay: even with SSR, the page must wait for the client framework to hydrate before interaction.
Cloudflare’s Fragments Architecture
To solve these issues, Cloudflare proposes a fragment‑based architecture where the application consists of a fragment tree. The browser requests the root fragment, which communicates with child fragments to generate the final response. Each fragment runs in an isolated Worker, and the entire response is parallel and streamed.
Advantages
Security: critical interface requests run inside server‑side Workers, preventing exposure of sensitive frontend code.
SSR + CDN: server‑side rendering combined with CDN yields very high Lighthouse scores.
Eager interactivity: the page becomes interactive as soon as the response arrives, without waiting for a full hydration cycle (requires framework support, e.g., Qwik).
Implementation Details
1. Streaming Aggregated Response
The entry point is a gateway Worker that distributes traffic and aggregates fragment streams.
fetch = async (request: Request, env: Env, ctx: ExecutionContext): Promise<Response> => {
this.validateFragmentConfigs(env);
request = await this.createSSRMessageBusAndUpdateRequest(request, env, ctx);
// Distribute fragment static assets
const fragmentResponse = await this.handleFragmentFetch(request, env);
if (fragmentResponse) return fragmentResponse;
const fragmentAssetResponse = await this.handleFragmentAssetFetch(request, env);
if (fragmentAssetResponse) return fragmentAssetResponse;
// Aggregate streaming response
const htmlResponse = await this.handleHtmlRequest(request, env, ctx);
if (htmlResponse) return htmlResponse;
return this.forwardFetchToBaseApp(request, env);
};handleHtmlRequest combines the legacy index.html with fragment streams:
// Aggregate legacy index.html and Fragments html
return this.returnCombinedIndexPage(
indexBody,
concatenateStreams(fragmentStreamsToInclude)
); export function concatenateStreams(streams: ReadableStream[]): ReadableStream {
async function writeStreams(writer: WritableStreamDefaultWriter) {
try {
for (const stream of streams) {
const reader = stream.getReader();
let chunk = await reader.read();
while (!chunk.done) {
writer.write(chunk.value);
chunk = await reader.read();
}
}
writer.close();
} catch (error) {
writer.abort(error);
}
}
const { writable, readable } = new TransformStream();
const writer = writable.getWriter();
writeStreams(writer);
return readable;
}Each fragment is wrapped in a piercing-fragment-host tag containing a full HTML string (including head and body).
2. Ensuring Correct Placement of Early‑Rendered Fragments
Fragments are rendered as soon as possible and placed in the right location using two custom web components: piercing-fragment-host: hosts the fragment’s HTML and activates it via Qwik or similar. piercing-fragment-outlet: placed in the legacy app; when activated it moves the corresponding host into the outlet’s DOM tree.
Pre‑piercing styles are injected to avoid visual flicker before the fragment is moved.
gateway.registerFragment({
fragmentId: "todos",
prePiercingStyles: `
:not(piercing-fragment-outlet) > piercing-fragment-host[fragment-id="todos"] {
position: absolute;
top: 25.65rem;
left: 0;
right: 0;
}
@media (max-width: 52rem) {
:not(piercing-fragment-outlet) > piercing-fragment-host[fragment-id="todos"] { top: 25.84rem; }
}
/* additional media queries omitted for brevity */
`,
});3. Fragment Communication
Fragments communicate via a framework‑agnostic MessageBus.
const currentUser = getBus().latestValue<{ username: string }>("authentication")?.username ?? null;
const todoListName = getBus().latestValue<{ name: string }>("todo-list-selected")?.name ?? null;
const requestCookie = request.headers.get("Cookie") || "";
const todoList = await getCurrentTodoList(requestCookie, currentUser, todoListName); useEffect(() => {
if (ref.current) {
return getBus(ref.current).listen<{ name: string }>("todo-list-selected", async (listDetails) => {
if (listDetails) {
const list = await getTodoList(currentUser, listDetails.name);
if (list) {
setListName(list.name);
setTodos(list.todos);
}
}
});
}
}, [ref.current]);Additional Considerations
Placeholder styles are needed before fragment piercing to position fragments correctly on various screen sizes.
Side‑effectful code in fragments is re‑executed on load using an addDefaultFnExportToBundle plugin that exports a default function and invokes it each time the fragment loads.
All fragments share the same domain, so relative static asset URLs must be prefixed (e.g., /_fragments/todos) or a dynamic publicPath must be set at runtime.
Takeaways
Recent interest from cloud providers like AWS and Cloudflare shows that micro‑frontends have matured beyond a buzzword; the complexity of the problem now justifies commercial, cloud‑native solutions.
Alipay Experience Technology
Exploring ultimate user experience and best engineering practices
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.
