Clear and concise description of the problem
As a developer using Module Federation with the bridge (@module-federation/bridge-react / @module-federation/bridge-vue3), I want application-level remotes to be fully server-side renderable — not just client-side mounted — so that a host (an Express/Nitro custom server, Modern.js, Next, Nuxt, Astro, TanStack Start, …) can render a federated remote application to HTML on the server, ship it in the document, and hydrate it on the client with working, URL-synced routing.
Today createBridgeComponent / createRemoteAppComponent are CSR-only: a server-rendered host shows a loading placeholder where the remote should be, which hurts first paint, SEO, and no-JS rendering. Module Federation already supports loading remotes in Node — what's missing is an SSR contract at the bridge level.
I intend to submit a PR for this — the implementation is complete and CI-green on my fork, including a runnable demo and Playwright e2e suite. I'll link the draft PR here.
Suggested solution
Extend the existing bridge provider contract with an optional server entry point, keeping the current render/destroy API untouched:
- Remote side: providers created by
createBridgeComponent additionally expose renderToString(info) => Promise<{ html, state }> (React via react-dom/server, Vue via @vue/server-renderer — both lazy-loaded so CSR-only client bundles are unaffected).
- Host side: a new
renderRemoteAppToString({ loader, moduleName, basename, url, ... }) helper (in bridge-react/ssr and re-exported from bridge-vue3) calls the remote's renderToString through loadRemote on the server and returns html, serialized state, and optional assets.
- Hydration:
createRemoteAppComponent accepts ssrHtml/ssrState; when present the bridge hydrates (hydrateRoot / createSSRApp + mount(dom, true)) instead of client-rendering. State is transferred via a cycle-safe, </script>-escaped JSON script, with bridge metadata namespaced under __mfBridge so it can't collide with app props. SSR metadata such as requestHeaders is stripped and never reaches the client.
- URL-driven routing: passing
url + basename renders the remote through StaticRouter (react-router v5/v6/v7 shims) or vue-router server-side, and hydrates into BrowserRouter / web history — so in-remote navigation updates the address bar and deep links SSR the correct route. The existing memoryRoute mode is kept for embedded remotes.
- Config: a new
bridge.ssr: boolean option on ModuleFederationPlugin aliases router imports to the SSR-aware shims.
The PR includes unit tests per router version, a federated Rsbuild + Express demo (apps/bridge-ssr-demo) with React and Vue remotes, and a Playwright suite covering no-JS rendering, deep-link SSR, hydration interactivity, SPA navigation, and address-bar sync.
Known limitations (documented in the PR): the contract is renderToString-based (no streaming yet — the API shape leaves room for a later renderToStream), and react-router data-router loaders are not executed during bridge SSR (components render with empty loader data and fetch after hydration).
Alternative
- Component-level federated SSR (current ecosystem answer, e.g. in Modern.js hosts): works well for federated components, but application-level bridge remotes (
createBridgeComponent — an entire app with its own router) remain CSR-only even there. This proposal makes application-level modules themselves SSR-able at the bridge layer, so it benefits Modern.js hosts just as much as custom Node/Express/Nitro servers or other frameworks.
- iframe or edge-side includes: avoids the hydration problem but gives up shared runtime, routing integration, and props.
- Each team hand-rolls
loadRemote + renderToString: possible today, but everyone re-solves state transfer, XSS-safe serialization, basename handling, and hydration mismatches — exactly the class of bugs the bridge exists to solve once.
Clear and concise description of the problem
As a developer using Module Federation with the bridge (
@module-federation/bridge-react/@module-federation/bridge-vue3), I want application-level remotes to be fully server-side renderable — not just client-side mounted — so that a host (an Express/Nitro custom server, Modern.js, Next, Nuxt, Astro, TanStack Start, …) can render a federated remote application to HTML on the server, ship it in the document, and hydrate it on the client with working, URL-synced routing.Today
createBridgeComponent/createRemoteAppComponentare CSR-only: a server-rendered host shows a loading placeholder where the remote should be, which hurts first paint, SEO, and no-JS rendering. Module Federation already supports loading remotes in Node — what's missing is an SSR contract at the bridge level.I intend to submit a PR for this — the implementation is complete and CI-green on my fork, including a runnable demo and Playwright e2e suite. I'll link the draft PR here.
Suggested solution
Extend the existing bridge provider contract with an optional server entry point, keeping the current
render/destroyAPI untouched:createBridgeComponentadditionally exposerenderToString(info) => Promise<{ html, state }>(React viareact-dom/server, Vue via@vue/server-renderer— both lazy-loaded so CSR-only client bundles are unaffected).renderRemoteAppToString({ loader, moduleName, basename, url, ... })helper (inbridge-react/ssrand re-exported frombridge-vue3) calls the remote'srenderToStringthroughloadRemoteon the server and returnshtml, serializedstate, and optional assets.createRemoteAppComponentacceptsssrHtml/ssrState; when present the bridge hydrates (hydrateRoot/createSSRApp+mount(dom, true)) instead of client-rendering. State is transferred via a cycle-safe,</script>-escaped JSON script, with bridge metadata namespaced under__mfBridgeso it can't collide with app props. SSR metadata such asrequestHeadersis stripped and never reaches the client.url+basenamerenders the remote throughStaticRouter(react-router v5/v6/v7 shims) or vue-router server-side, and hydrates intoBrowserRouter/ web history — so in-remote navigation updates the address bar and deep links SSR the correct route. The existingmemoryRoutemode is kept for embedded remotes.bridge.ssr: booleanoption onModuleFederationPluginaliases router imports to the SSR-aware shims.The PR includes unit tests per router version, a federated Rsbuild + Express demo (
apps/bridge-ssr-demo) with React and Vue remotes, and a Playwright suite covering no-JS rendering, deep-link SSR, hydration interactivity, SPA navigation, and address-bar sync.Known limitations (documented in the PR): the contract is
renderToString-based (no streaming yet — the API shape leaves room for a laterrenderToStream), and react-router data-router loaders are not executed during bridge SSR (components render with empty loader data and fetch after hydration).Alternative
createBridgeComponent— an entire app with its own router) remain CSR-only even there. This proposal makes application-level modules themselves SSR-able at the bridge layer, so it benefits Modern.js hosts just as much as custom Node/Express/Nitro servers or other frameworks.loadRemote+renderToString: possible today, but everyone re-solves state transfer, XSS-safe serialization, basename handling, and hydration mismatches — exactly the class of bugs the bridge exists to solve once.