Privacy Policy© 2026 DEV BAK - TECH BLOG. All rights reserved.
DEV BAK - TECH BLOG
frontend

Implementing Cloudflare Workers for Edge MFE Orchestration to Reduce TTFB

Honestly, when I first heard about micro-frontends (MFE), my immediate thought was, "Independent deployments per team? Sounds great — but doesn't that mean the user's browser has to download every team's bundle separately?" Anyone who has experienced the chronic pain of client-side composition — slow initial rendering as multiple MFE JS bundles load sequentially — will relate. Server-side composition is a bit better, but then the central server becomes a bottleneck and the core MFE value of independent deployment gets blurry.

Edge MFE runtime orchestration is an approach that overcomes both of these limitations simultaneously. It assembles each team's HTML and JS fragments at CDN edge nodes closest to the user and delivers them as a single response. Real-world references report noticeably reduced TTFB compared to central-server-based approaches (results vary by conditions and infrastructure configuration). This article explores why this paradigm is attracting attention now, how to actually configure a Router Worker with Cloudflare Workers, and the structure where ESI and a Manifest Resolver form a single system. It will be especially useful for those currently running client-side MFEs and looking to improve TTFB.


Core Concepts

Three Composition Approaches — Why Edge Is Different

How you compose MFEs broadly divides into three layers. The difference becomes clear when you look at where each approach handles assembly.

Approach Assembly Location Characteristics Main Drawbacks
Client-side composition Browser Maximum flexibility Slow initial load, bundle bloat
Server-side composition Central server SEO-friendly Central server bottleneck, difficult independent deployment
Edge-side composition CDN edge node Minimized TTFB, maintains independent deployment Operational complexity, state management difficulty

The key to edge composition: Edge nodes within 50ms of the user collect each team's MFE fragments and assemble them into a single integrated response. Without routing through a central server, round-trip latency is reduced, and each team still maintains independent deployment.

Core Components of Orchestration

To understand edge MFE orchestration, five concepts are worth knowing.

Router Worker is the gateway that routes incoming requests to the appropriate MFE Worker based on URL path. Service Binding is a Cloudflare-specific feature that enables direct communication between Workers within the same data center without HTTP round-trips. The key point isn't simply "it doesn't go through the public internet" — it's that there's no separate network round-trip at all, so latency approaches nearly zero compared to regular HTTP calls.

Remote Entry / Manifest is a spec that dynamically declares MFE bundle locations at runtime. ESI (Edge Side Include) is a markup technology that dynamically composes content at the CDN level using <esi:include> tags. Import Map may be unfamiliar — for backend developers, think of it as similar to the imports field in package.json. It's a browser-native module path declaration that maps statements like import 'team-a/App.js' to actual CDN URLs when the browser encounters them.

Module Federation 3.0 and Native ESM Federation

I used to think Module Federation was a Webpack-bound technology, but as of 2026, the story has changed considerably. Native ESM Federation federates MFEs using only browser-native import(), Import Maps, and Top-Level Await — without any Webpack build process.

html
<!-- Native ESM Federation using Import Maps -->
<script type="importmap">
{
  "imports": {
    "shell/": "https://cdn.example.com/shell/v2.1.4-a3f8c9d/",
    "team-a/": "https://cdn.example.com/team-a/v1.8.2-b7e2f1a/",
    "team-b/": "https://cdn.example.com/team-b/v3.0.1-c9d4e8b/"
  }
}
</script>
<script type="module">
  const { default: TeamAApp } = await import('team-a/App.js');
  const { default: TeamBApp } = await import('team-b/App.js');
 
  TeamAApp.mount('#section-a');
  TeamBApp.mount('#section-b');
</script>

The reason version hashes are included in URLs is CDN cache busting. The path must change with each deployment so the CDN reliably serves the new version.

One thing to watch out for is Import Maps' browser support status. Natively supported in Safari 16.4 and above and Chrome 89 and above — if you need to support older versions, polyfills like es-module-shims are required. Module version conflicts — for example, when team-a and team-b each require different versions of React — can be addressed by enforcing a single version at the Import Map level, or by isolating each MFE into a fully isolated Shadow DOM context.

Version Management via Manifest Endpoint

In the past, remoteEntry.js was referenced statically, but now the approach of dynamically resolving versions at runtime via a Manifest Endpoint is becoming widespread.

json
// GET /manifest.json — handled by an edge Worker
{
  "version": "2.4.1",
  "entries": {
    "team-a": {
      "url": "https://cdn.example.com/team-a/v1.8.2-b7e2f1a/remoteEntry.js",
      "integrity": "sha384-abc123...",
      "canary": false
    },
    "team-b": {
      "url": "https://cdn.example.com/team-b/v3.0.1-canary.3-c9d4e8b/remoteEntry.js",
      "integrity": "sha384-def456...",
      "canary": true,
      "canaryPercentage": 10
    }
  }
}

This structure makes it natural to handle canary deployments, A/B testing, and gradual rollouts at the edge layer. From a deployment perspective, it's quite convenient — you can control traffic by changing only the Manifest without any code changes.


Practical Implementation

Now that we understand the concepts, let's look at how to actually implement this. The three examples below are not independent code snippets — they are layers that form a single system.

The Router Worker receives all requests and routes them to team-specific Workers. Some of those team Workers return HTML responses containing ESI markup. Which version of the client bundle to load from that HTML is determined by the Manifest Resolver. The three components connect in sequence to create the flow: "request routing → server fragment assembly → client bundle version resolution."

Example 1: Configuring Path-Based Routing with Router Worker

Since Cloudflare's official VMFE template was released, implementation patterns have become much clearer. The structure has the Router Worker look at the URL path and directly call each team's Worker via Service Binding. To be production-ready, fallback handling when a team Worker doesn't respond and request header forwarding must not be omitted.

typescript
// router-worker/src/index.ts
export interface Env {
  TEAM_A_WORKER: Fetcher; // Service Binding: /products path
  TEAM_B_WORKER: Fetcher; // Service Binding: /orders path
  SHELL_WORKER: Fetcher;   // Service Binding: shared layout
}
 
export default {
  async fetch(request: Request, env: Env): Promise<Response> {
    const url = new URL(request.url);
 
    // Forward original headers so team Workers can use auth and region info
    const forwardRequest = new Request(request);
 
    try {
      if (url.pathname.startsWith('/products')) {
        return await withTimeout(env.TEAM_A_WORKER.fetch(forwardRequest), 3000);
      }
 
      if (url.pathname.startsWith('/orders')) {
        return await withTimeout(env.TEAM_B_WORKER.fetch(forwardRequest), 3000);
      }
 
      return await env.SHELL_WORKER.fetch(forwardRequest);
    } catch (err) {
      // Explicit 503 response on team Worker failure (prevents infinite waiting)
      const message = err instanceof Error ? err.message : 'upstream error';
      return new Response(`Service unavailable: ${message}`, { status: 503 });
    }
  },
};
 
function withTimeout(promise: Promise<Response>, ms: number): Promise<Response> {
  return Promise.race([
    promise,
    new Promise<Response>((_, reject) =>
      setTimeout(() => reject(new Error(`timeout after ${ms}ms`)), ms)
    ),
  ]);
}
toml
# wrangler.toml — Service Binding declaration
name = "router-worker"
 
[[services]]
binding = "TEAM_A_WORKER"
service = "team-a-worker"
 
[[services]]
binding = "TEAM_B_WORKER"
service = "team-b-worker"
 
[[services]]
binding = "SHELL_WORKER"
service = "shell-worker"
Code Point Description
Fetcher type Service Binding interface. Zero-latency communication within the same data center, no HTTP round-trip
withTimeout Prevents the entire router from stalling when a team Worker is unresponsive
new Request(request) Forwards original headers (cookies, auth tokens, CF-IPCountry, etc.) to downstream Workers as-is
[[services]] Declares each team Worker as an independent service for separate deployment

Example 2: Mixing Legacy Systems and MFE with ESI

If you're already using a CDN that supports ESI — such as Akamai, Cloudflare, or Fastly — you can introduce MFEs incrementally without replacing the legacy system all at once. This is the scenario encountered most often in practice. For Cloudflare, to process ESI in Workers response bodies, you need to disable the cf: { minify: { html: false } } setting and attach Surrogate-Control: ESI/1.0 to the response headers.

html
<!-- ESI markup processed at the CDN layer -->
<!DOCTYPE html>
<html>
<head>
  <title>My App</title>
  <esi:include src="https://legacy.example.com/fragments/header" />
</head>
<body>
  <!--
    onerror="continue": if this fragment request fails, the rest of the page renders normally.
    ESI requests are processed in parallel by the CDN, so there's no sequential delay.
    Note: timeouts must be adjusted separately in CDN vendor settings (Cloudflare default is 30s).
  -->
  <esi:include src="https://mfe-team-a.example.com/fragment/product-list"
               onerror="continue" />
 
  <div id="legacy-content">
    <!-- Content rendered from the existing server -->
  </div>
 
  <esi:include src="https://mfe-team-b.example.com/fragment/cart"
               onerror="continue" />
</body>
</html>
typescript
// team-a/src/fragment.ts — fragment endpoint independently operated by Team A
export default {
  async fetch(request: Request): Promise<Response> {
    const html = await renderProductList();
 
    return new Response(html, {
      headers: {
        'Content-Type': 'text/html',
        // Cache for 1 minute, then refresh smoothly via stale-while-revalidate
        'Cache-Control': 'public, max-age=60, stale-while-revalidate=300',
        // Cache for 10 minutes at the CDN (Surrogate) layer
        'Surrogate-Control': 'max-age=600',
      },
    });
  },
};
Code Point Description
onerror="continue" Isolates a specific fragment failure so it doesn't block the entire page from rendering
ESI parallel processing The CDN issues <esi:include> requests in parallel — not a sequential chain
stale-while-revalidate Returns the previous version immediately after cache expiry, while refreshing in the background
Independent endpoint Each team only manages their own Fragment URL, enabling independent deployment

Example 3: Controlling Canary Deployments with Manifest Resolver

An edge Worker can dynamically decide which MFE version to deliver based on the characteristics of the requester. Where this example connects to the Router Worker → ESI fragment flow is that, after the client receives the page, it additionally requests /manifest.json to determine which JS bundle to load.

The charCode summation approach used in earlier drafts has uneven distribution, causing the actual canary percentage to differ. Using the FNV-1a hash below always produces the same result for the same userId while achieving a much more uniform distribution across 100 buckets.

typescript
// manifest-resolver/src/index.ts
export default {
  async fetch(request: Request): Promise<Response> {
    const userId = request.headers.get('X-User-Id') ?? '';
    const isCanaryUser = shouldReceiveCanary(userId, 10); // 10% canary
 
    const manifest = {
      'team-a': isCanaryUser
        ? 'https://cdn.example.com/team-a/v1.9.0-canary.1-d3a8f2c/remoteEntry.js'
        : 'https://cdn.example.com/team-a/v1.8.2-b7e2f1a/remoteEntry.js',
      'team-b': 'https://cdn.example.com/team-b/v3.0.1-c9d4e8b/remoteEntry.js',
    };
 
    return Response.json(manifest, {
      headers: { 'Cache-Control': 'no-store' }, // personalized responses must not be cached
    });
  },
};
 
function shouldReceiveCanary(userId: string, percentage: number): boolean {
  // FNV-1a 32-bit: more uniform bucket distribution than simple charCode summation
  let hash = 2166136261;
  for (let i = 0; i < userId.length; i++) {
    hash ^= userId.charCodeAt(i);
    hash = Math.imul(hash, 16777619) >>> 0;
  }
  return (hash % 100) < percentage;
}
Code Point Description
FNV-1a hash Same userId → always the same bucket. The version doesn't change every time the user refreshes
Math.imul(...) >>> 0 Safely handles 32-bit integer arithmetic in JS
no-store Personalized responses must not be cached by the CDN. Only the Manifest is no-store; bundles use a separate caching strategy

Pros and Cons Analysis

Advantages

Item Details
Ultra-low latency Runs at the edge closest to the user. TTFB significantly improved by eliminating central server round-trips
Independent deployment Full freedom of per-team CI/CD pipelines and technology stack choices
High availability Eliminates single points of failure due to the distributed nature of edge nodes
Minimized cold starts V8 Isolate-based architecture for millisecond-level cold starts
Flexible caching Independent caching strategies per fragment when dynamic and static content coexist

Disadvantages and Caveats

Item Details Mitigation
Operational complexity Management burden of multiple edge Workers and routing rules Automate Worker declarations with IaC such as Terraform, Pulumi
CDN dependency Limited CDN support for ESI (Akamai, Cloudflare, Fastly, Varnish) Minimize CDN dependency with Import Maps-based Native Federation
Statelessness constraint Edge functions don't maintain state, making session management complex Use edge storage like Cloudflare KV, Durable Objects
Local development difficulty Hard to reproduce the edge environment (Web API only) locally Set up Miniflare or Wrangler local mode from the start
Vendor fragmentation ESI behavior differs subtly across CDN vendors Minimize dependency on vendor-specific features; use a common abstraction layer
Organizational maturity required Without clear API contracts and inter-team governance, benefits are diminished Document interface contracts; establish a shared design system first

V8 Isolate: An isolated execution environment provided by the Chromium V8 engine. It can start in tens of milliseconds without containers, making it the core technology that solves the cold start problem for edge functions.

stale-while-revalidate: An HTTP caching directive. After the cache expires, it immediately returns the previous version while fetching the new version in the background. Users always receive a fast response.

The Most Common Mistakes in Practice

  1. Mixing ESI and client-side composition without unifying caching policy. When our team first introduced ESI, we experienced a situation where different versions were stored in server, edge, and browser caches respectively — causing certain users to see the cart UI rendered with a different version than the header. Explicitly declaring both Surrogate-Control and Cache-Control was the first item on our checklist.

  2. Introducing edge orchestration without inter-team API contracts. If you start without prior agreement on what interfaces each team's fragments should expose, the Router Worker quickly fills up with per-team exception handling. Defining fragment URL patterns, caching header specs, and error response formats in JSON Schema first makes everything much easier later.

  3. Postponing local development environment setup. The edge environment has the constraint of only using Web APIs — Node.js APIs are unavailable — and discovering this right before deployment makes debugging extremely painful. Setting up wrangler dev or Miniflare at the start of the project prevents this problem proactively.


Closing Thoughts

Edge MFE runtime orchestration becomes the most powerful option when simultaneously pursuing both team independence and user experience. However, there's one thing to honestly ask yourself before adopting this technology. For internal tools with low traffic, or small products with only one or two development teams, the complexity of operating multiple edge Workers may cost more than the benefits it provides. "Are there actually multiple teams that need to deploy independently?" is the fastest criterion for judging whether this technology is the right fit.

Three steps you can start with right now:

  1. You can clone Cloudflare's official VMFE template and run it locally. Starting with the command npm create cloudflare@latest -- --template=cloudflare/workers-sdk/templates/worker-mfe generates a Router Worker along with two sub-Workers, letting you immediately experience the Service Binding structure firsthand.

  2. It's recommended to identify one small area in your currently running monolith that can be deployed independently and separate it out as an ESI fragment. A good first candidate is a UI block owned by a different team or one with a noticeably different update frequency from the rest of the page. For example, if the header is frequently changed by the design team while the product listing is managed by the commerce team, separating just the header as an ESI fragment allows each to deploy independently. The key is finding an area where two sections with different change cycles are bundled into a single deployment unit.

  3. It's worth documenting inter-team MFE interface contracts as a Manifest Endpoint spec. Defining in JSON Schema which paths return which fragments and what the caching policies are makes configuring the orchestration layer much smoother afterward.


References

  • Micro-frontends 2026: Module Federation 3.0 & Native ESM Federation
  • Cloudflare Launches Vertical Microfrontend Template for Path-Based Edge Routing | InfoQ
  • Building vertical microfrontends on Cloudflare's platform
  • Microfrontends · Cloudflare Workers docs
  • Approaching Micro-frontend Orchestration | Mia-Platform
  • Edge-Rendered Frontends with CDN Functions: Balancing Performance, Cost, and Complexity
  • Micro Frontends Patterns#10: Edge Side Includes | DEV Community
  • Micro Frontends Patterns#11: Edge Side Composition | DEV Community
  • MFE Orchestrator - Microfrontend Versioning & Orchestration
  • Module Federation Runtime Guide
  • Composing pages and views with micro-frontends | AWS Prescriptive Guidance
#MicroFrontends#CloudflareWorkers#엣지컴퓨팅#ESI#ModuleFederation#ImportMap#ServiceBinding#NativeESM#TypeScript#카나리배포
Share

Table of Contents

Core ConceptsThree Composition Approaches — Why Edge Is DifferentCore Components of OrchestrationModule Federation 3.0 and Native ESM FederationVersion Management via Manifest EndpointPractical ImplementationExample 1: Configuring Path-Based Routing with Router WorkerExample 2: Mixing Legacy Systems and MFE with ESIExample 3: Controlling Canary Deployments with Manifest ResolverPros and Cons AnalysisAdvantagesDisadvantages and CaveatsThe Most Common Mistakes in PracticeClosing ThoughtsReferences

Recommended Posts

15 Frontend Accessibility Checklist Items After EAA Takes Effect (European Accessibility Act WCAG 2.1 AA Standard)
frontend

15 Frontend Accessibility Checklist Items After EAA Takes Effect (European Accessibility Act WCAG 2.1 AA Standard)

On June 28, 2025, I stopped while reading a message that came through on Slack: "EAA has taken effect — is our service okay?" At first I thought, "It's an EU la...

June 26, 202626 min read
Removing Popper.js with CSS Anchor Positioning — Floating UI Handled Directly by the Browser
frontend

Removing Popper.js with CSS Anchor Positioning — Floating UI Handled Directly by the Browser

Anyone who has done frontend work eventually thinks to themselves: "Why is attaching a dropdown below a button this complicated?" When I first built my team's d...

June 26, 202621 min read
Vite 8 + Rolldown: How a Dual-Bundler Integration Cut Production Build Times by 87%
frontend

Vite 8 + Rolldown: How a Dual-Bundler Integration Cut Production Build Times by 87%

(Vite 8 Rolldown migration | build speed) When I first heard that a 46-second build had dropped to 6 seconds, I was honestly skeptical. Benchmark numbers alw...

June 26, 202617 min read
TanStack DB in Practice: How a Client-Side DB Changes Optimistic Updates
frontend

TanStack DB in Practice: How a Client-Side DB Changes Optimistic Updates

Honestly, when I first heard "client-side embedded DB," my reaction was "just another buzzword." TanStack Query already handles server state management well — w...

June 23, 202617 min read
Tailwind v4 @container — Responsive Components That Hold Up in Sidebars and Grids Alike
frontend

Tailwind v4 @container — Responsive Components That Hold Up in Sidebars and Grids Alike

At some point in responsive UI work, you hit a wall. A card component you carefully crafted works perfectly in the main feed, but the moment you drop the same c...

June 23, 202618 min read
When Linting Gets 62x Faster, Your Development Habits Change — Migrating from ESLint to Oxlint in a Vite Project
frontend

When Linting Gets 62x Faster, Your Development Habits Change — Migrating from ESLint to Oxlint in a Vite Project

Have you ever felt that your linter is slow? I did too. I used to think waiting nearly 30 seconds every time I ran on a mid-sized React project was just normal...

June 23, 202619 min read