DEV Community

Cover image for Next.js 11, Module Federation, and SSR — A whole new world
Zack Jackson
Zack Jackson

Posted on • Originally published at scriptedalchemy.Medium

Next.js 11, Module Federation, and SSR — A whole new world

Server-side rendering with Next 11 and Module federation is ready for prime time! Bonus: we got "hot" (live) reloading working on federated applications!

Its been a long and painful road to module federation inside a next.js application — but we have finally been able to remove most of that pain.

This is the first time I can confidently say that we have been able to achieve full-scale Module Federation in Next.js — not just client-side but server-side as well!

Months ago, we demonstrated the concept of code-streaming, the idea was to make Node work like a browser and download remote chunks, executing them under its process. This was seamless and easy but posed some security concerns and in the case of next.js, where we are unable to "hot reload" production, those required chunks would get "stuck" and your host applications need to be restarted in order to re-fetch new remote chunks what were updated.

Say hello to multi-server rendering!

The Module Federation Group has conjured up a more acceptable solution to solve Federated SSR. Our solution doesn’t just work with Next.js applications — but this article is going to focus on utilizing our new system within the context one Next.js

Jacob outlines more details around the underlaying architecture
Webpack Federation SSR | ebey.me
*Webpack Module Federation has been a game changer in the micro-frontend space allowing multiple SPA's to operate as one…*www.ebey.me

Huge credit and shout out to Jacob Ebey. A critical part and mastermind behind this architecture

Multi-server rendering is exactly what it sounds like, the remotes accept props from the host application and perform the render at their origin, sending back the pre-rendered HTML. The whole process is blazing fast and if you put it behind a CDN, its even faster. Since rendering a single component only takes around 10ms–20ms, and our parser only adds 5–7ms of overhead on larger payloads.

Once we get the markup from the remote origin, our parser converts the HTML back into React, if you are passing host-originated children inside a remote component, those children are re-connected to the host render tree and render context. So they are still able to participate in any app-specific context that takes place during the render cycle.

What’s important to note is that we are able to asynchronously render any remote components at their origins simultaneously, so an RTT waterfall does not begin to develop, the response time is as fast as the slowest response — just like a Promise.all

import {
  getOrCreateFederatedComponentCtx,
  federatedComponentsContext,
  federatedComponent,
} from "nextjs-shared";

import remotes from "../remotes";

const Header = federatedComponent("home", "./header");

function MyApp({ Component, pageProps, federatedComponentsCtx, shellData }) {
  return (
    <federatedComponentsContext.Provider
      value={getOrCreateFederatedComponentCtx(
        { remotes },
        federatedComponentsCtx
      )}
    >
      <Header items={shellData.header.menu} />
      <Component {...pageProps} />
    </federatedComponentsContext.Provider>
  );
}
Enter fullscreen mode Exit fullscreen mode

Hydration and avoiding flicker

User experience needs to be seamless, so we cannot have any flicker happen during hydration. In order to achieve this, we utilize a mechanism similar to react-lazy-hydration, where we only hydrate parts of the react tree from HTML into interactive React at startup. This preserves the existing markup and suppresses any hydration problems that may happen. Once Webpack kicks in and the federated code is executed, the SSR’d remote module is then hydrated into a federated react component.

I use react-lazy-hydration quite extensively to improve performance, offloading expensive components so they are only hydrated when TTI is reached or that are about to be scrolled into view with intersection observer. So applying a similar tactic to federated SSR is perfectly acceptable to me, and very effective.

Module Federation and Chunk Flushing are back!

One big request from the community has been that packages like react-loadable or loadable-components are unable to "flush" out and render federated Javascript tags and style sheets that were used. Causing additional RTT as the remote container kicks in, and has to go download these additional JS chunks right away.

Our new solution solves this problem, server-side rendered federated modules also flush out their JS and CSS chunks under our latest architecture design. Order to the world has been restored!

While other third parties do not support federated chunk flushing, when using our fetchFederatedComponent function, the flush will happen as expected and we handle flushing of federated chunks independently. If you use code splitting, keep using loadable-components for your internal chunks but you would be able to use our federated functions for getting those critical assets rendered to the DOM during SSR.

We place the CSS and js chunks into next.js context so next is aware of these additional scripts the whole time — ensuring there's no flicker during rehydration, no additional RTT to slow down your load times.

Making Module Federation and Next.js play nice

Next.js does not support Module Federation out of the box, you will need to depend on a plugin from the Module Federation Group which provides support. As far as I am aware, there are no near-future plans to see Next.js support Module Federation out of the box and when it does arrive — there are slim chances that SSR support will be available. At the time of writing, I believe that the MF Group is the only organization that possesses the technology to offer a "no limits" capability on Next's platform.

The nextjs-mf plugin enables Module Federation on the client-side, regardless of SSR you will need this in order for Federation to work at all.

The Case for Module Federation on Next.js applications

Next.js is not modular, it's very monolithic and mostly focuses on faster monolithic builds. But this pattern isn’t sustainable, Next Zones attempts to get closer to a more modular setup, but you are still locked in at page verticals and cannot escape a full page reload when switching zones.

Federation enables us to make Next.js highly modular. You can develop a MegaNav that can be deployed independently of the rest of your application, or split teams up in a more granular and autonomous manner. Teams can own components and are not chained to a page, and those components can be easily distributed across other pages without needing to re-deploy an entire Next.js instance or multiple instances. You don't have to depend on npm package bumps as a way to distribute shared code and you don’t have to depend on third-party vendors for patterns like ESI in order to get some modularity.

With server-side rendering solved, there are no drawbacks to leveraging module federation to tame and scale Next.js far more effectively.

No Page Reloads!!

Next Zones had the right idea but it's time to wave goodbye to page reloads when jumping between apps and instead welcome Federated Page Routing.

And the 97 lighthouse score speaks for itselfAnd the 97 lighthouse score speaks for itself

The core concept here is that you stay on the host application and instead federate the page into that application, not reload, and SSR another page from another application. SSR costs money, avoiding a page refresh and instead, leveraging client-side routing to simply pull the page from another application is faster, cheaper, and a better user experience.

To make this possible, we take advantage of the mechanics of Module Federation and next.js a CatchAll route.

We have a primitive example of omnidirectional, distributed routing available on Github.
module-federation-examples/nextjs at master · module-federation/module-federation-examples
*Module Federation in Next.js depends on @module-federation/nextjs-mf It will not work unless you have access to this…*github.com

Each application exposes a route map via module federation and the CatchAll page federates all the remote route-maps together, then uses some simple pattern recognition to see if the current path matches any existing mapped routed from a specific remote, if it does we call that remote and get the federated page — if we are unable to resolve the route, or if a JS error occurs for any reason, the CatchAll route simply performs a window.location.reload() which would bounce the user back through your application infrastructure and either land them on another SSR route that wasn’t mapped, an external URL, or a 404 page.

This mechanism and how we designed the federation plugin for Next.js ensures that even if federated pages have an error, the Next.js route will not, guaranteeing that navigating between pages will always work, and in the worst-case falling back to a page reload.

// [...slug].js
import { createFederatedCatchAll } from "nextjs-shared"; export default createFederatedCatchAll(["checkout", "home"]);
Enter fullscreen mode Exit fullscreen mode

and the magic behind createFederatedCatchAll

export function createFederatedCatchAll(remotes) {
const FederatedCatchAll = (initialProps) => {
const [lazyProps, setProps] = React.useState({});
const { FederatedPage, render404, renderError, needsReload, ...props } = {
  ...lazyProps,
  ...initialProps,
};

React.useEffect(async () =&gt; {
  if (needsReload) {
    const federatedProps = await FederatedCatchAll.getInitialProps(props);
    setProps(federatedProps);
  }
}, []);

if (render404) {
  // *TODO: Render 404 page
  *return React.createElement("h1", {}, "404 Not Found");
}
if (renderError) {
  // *TODO: Render error page
  *return React.createElement("h1", {}, "Oops, something went wrong.");
}

if (FederatedPage) {
  return React.createElement(FederatedPage, props);
}

return null;
Enter fullscreen mode Exit fullscreen mode

};

FederatedCatchAll.getInitialProps = async (ctx) => {
const { err, req, res, AppTree, ...props } = ctx;

if (err) {
  // *TODO: Run getInitialProps for error page
  *return { renderError: true, ...props };
}

if (!***process***.browser) {
  return { needsReload: true, ...props };
}

try {
  const matchedPage = await matchFederatedPage(remotes, ctx.asPath);

  const remote = matchedPage?.value?.remote;
  const mod = matchedPage?.value?.module;

  if (!remote || !mod) {
    // *TODO: Run getInitialProps for 404 page
    *return { render404: true, ...props };
  }

  ***console***.log("loading exposed module", mod, "from remote", remote);
  try {
    if (!***window***[remote].__initialized) {
      ***window***[remote].__initialized = true;
      await ***window***[remote].init(__webpack_share_scopes__.default);
    }
  } catch (initErr) {
    ***console***.log("initErr", initErr);
  }

  const FederatedPage = await ***window***[remote]
    .get(mod)
    .then((factory) =&gt; factory().default);
  ***console***.log("FederatedPage", FederatedPage);
  if (!FederatedPage) {
    // *TODO: Run getInitialProps for 404 page
    *return { render404: true, ...props };
  }

  const modifiedContext = {
    ...ctx,
    query: matchedPage.params,
  };
  const federatedPageProps =
    (await FederatedPage.getInitialProps?.(modifiedContext)) || {};
  return { ...federatedPageProps, FederatedPage };
} catch (err) {
  ***console***.log("err", err);
  // *TODO: Run getInitialProps for error page
  *return { renderError: true, ...props };
}
Enter fullscreen mode Exit fullscreen mode

};

return FederatedCatchAll;
}

Enter fullscreen mode Exit fullscreen mode




Interested in Federated SSR? Contact us!

Our fetch implementation of SSR is not open source and took immense time and effort to develop, for any companies interested in leveraging our technology, we are happy to hold a workshop or consult, the Module Federation Group does accept clients. We are the creators of Module Federation

You can get in contact:

Zackary.l.jackson@gmail.com

or over Twitter

ScriptedAlchemy

ebey_jacob

Or book a workshop directly
Next.js Federated SSR Workshop - Zack Jackson
*Workshop plus private access to source code that contains a reference architecture for Federated SSR on Next.js - 8…*calendly.com

Top comments (1)

Collapse
 
rangercoder99 profile image
RangerCoder99

A whole new world
A new fantasic point of view
No one to tell us. 'No'
Or where to go
Or say we're only dreaming
A whole new world
A dazzing place I never knew
But when I'm way up here
It's crystal clear
That now I'm in a whole new world with you 😁