The Evolution of Frontend Complexity
Over the past decade, software engineering has largely embraced the microservices architecture on the backend. We split massive backend monoliths into smaller, independently deployable services based on business domains. However, while the backend evolved, the frontend was left behind. We created a new beast: The Frontend Monolith.
In massive enterprise web applications, it is common to find dozens of development teams pushing code to a single, gigantic React or Next.js repository. This creates severe bottlenecks. Build times skyrocket to 30+ minutes. A critical bug introduced by the "Checkout" team blocks the deployment of a crucial feature built by the "Marketing" team. Codebase navigation becomes a nightmare, and upgrading a core dependency like React or a UI library requires a massive, coordinated effort across the entire organization.
At Smart Tech Devs, when scaling web platforms for enterprise teams, we implement a Micro-Frontend Architecture. Just like backend microservices, micro-frontends allow independent teams to build, test, and deploy separate pieces of the user interface autonomously, assembling them seamlessly in the user's browser.
The Magic of Webpack Module Federation
Historically, implementing micro-frontends was clunky. Developers relied on iframes (which caused massive accessibility and styling issues) or complex build-time compositions via NPM packages (which still required the main host app to redeploy every time a package updated).
Everything changed with the release of Webpack 5 and a feature called Module Federation. Module Federation allows a JavaScript application to dynamically load code from another application at runtime. The host application doesn't need to know about the remote code at build time; it simply fetches it over the network when the user navigates to the page.
Architecting the Solution
To implement this in Next.js, we require two distinct types of applications:
- The Host (Shell) Application: The main container that handles global routing, authentication state, and the overall layout (headers, footers).
- The Remote Applications: Independent Next.js apps that expose specific components or pages (e.g., a standalone E-commerce Checkout app).
Step 1: Configuring the Remote Application
Let's say we have an independent application built by the Checkout team. They want to expose their CartWidget component so the Host application can display it in the global navbar. We utilize the @module-federation/nextjs-mf plugin in the remote application's next.config.js.
// Remote App (Checkout Team) - next.config.js
const NextFederationPlugin = require('@module-federation/nextjs-mf');
module.exports = {
webpack(config, options) {
config.plugins.push(
new NextFederationPlugin({
name: 'checkoutApp',
filename: 'static/chunks/remoteEntry.js',
exposes: {
// Exposing the CartWidget component to the world
'./CartWidget': './components/CartWidget.tsx',
},
shared: {
// Ensure React is shared as a singleton to prevent hooks crashing
react: { singleton: true, eager: false, requiredVersion: false },
'react-dom': { singleton: true, eager: false, requiredVersion: false },
},
})
);
return config;
},
};
Step 2: Configuring the Host Application
Now, we configure the main Host application to consume the remote code. We tell the Host where to find the remoteEntry.js file generated by the Checkout app.
// Host App (Shell) - next.config.js
const NextFederationPlugin = require('@module-federation/nextjs-mf');
module.exports = {
webpack(config, options) {
config.plugins.push(
new NextFederationPlugin({
name: 'hostApp',
remotes: {
// Defining the remote URL (dynamically assigned in production via env vars)
checkoutApp: `checkoutApp@http://localhost:3001/_next/static/chunks/remoteEntry.js`,
},
shared: {
react: { singleton: true, eager: false, requiredVersion: false },
'react-dom': { singleton: true, eager: false, requiredVersion: false },
},
})
);
return config;
},
};
Step 3: Dynamic Runtime Integration
With the configuration in place, we can now render the remote component inside our Host application. Because this component is fetched over the network at runtime, we must use Next.js dynamic imports wrapped in a React Suspense boundary to handle the loading state gracefully.
// Host App - components/Navbar.tsx
import dynamic from 'next/dynamic';
import { Suspense } from 'react';
// Dynamically import the remote component
const RemoteCartWidget = dynamic(
() => import('checkoutApp/CartWidget'),
{ ssr: false } // Best practice to render remote MF components client-side initially
);
export default function GlobalNavbar() {
return (
<nav className="flex justify-between items-center p-4 bg-gray-900 text-white">
<div className="logo">Smart Tech Devs</div>
{/* Render the remote component, showing a fallback while it downloads */}
<Suspense fallback={<div className="animate-pulse w-8 h-8 bg-gray-700 rounded"></div>}>
<RemoteCartWidget />
</Suspense>
</nav>
);
}
Handling Global State and Styling
The greatest challenge in Micro-Frontends is managing shared state (like a logged-in User object) and CSS conflicts. To manage state, the Host application usually holds the primary React Context Provider (or Zustand store). Because we defined React as a "singleton" in the Webpack configuration, the remote components will seamlessly hook into the Host's context. For styling, it is highly recommended to use scoped CSS solutions like CSS Modules, Tailwind CSS, or styled-components to ensure a class name in the "Checkout" app doesn't accidentally override the design of a button in the "Host" app.
The Engineering ROI
Micro-frontends fundamentally transform how large engineering departments operate. The "Checkout" team can now merge a PR, trigger their own isolated CI/CD pipeline, and deploy their app to production in 2 minutes. The moment their deployment finishes, the new CartWidget is instantly reflected on the main Host application without the Host ever needing to redeploy. You achieve true team autonomy, vastly reduced build times, and the ability to scale your frontend infrastructure to infinity.
Top comments (0)