Marcio Cunha

How to build modular micro-frontends with Vite and Module Federation

Learn how to decouple modern web applications using Module Federation and Vite, ensuring efficient dependency sharing across teams.

Marcio Cunha•6 min
Also available in:EspañolPortuguês
Summary
  • Micro-frontend architecture splits monolithic applications into smaller parts managed by independent teams.
  • Module Federation allows loading code at runtime directly from other web applications.
  • Vite speeds up development using on-demand compilation with native ES modules.
  • Dependency sharing prevents duplicating heavy libraries like React in the browser.
  • Rigorous versioning and clear contracts prevent unexpected breakages in production.

Why split your web dashboard into smaller parts

Imagine your company has grown, the engineering team has doubled in size, and everyone is trying to modify the exact same source code of the main system at the same time. In practice, this means constant Git conflicts, sluggish builds that take twenty minutes, and risky deployments where a bug in the payment module takes down the entire customer dashboard. Splitting the interface into smaller, independent pieces solves this human and technical scaling problem.

This decentralized approach, known as micro-frontends architecture, allows different teams to develop, test, and deploy isolated parts of the website or system without relying on a single giant repository. It is like a gated community where each house has its own routine, but all share the same main street. If a team wants to update the financial dashboard using the latest version of a charting library, they can do so without blocking the user onboarding team's work.

However, separating everything into distinct projects brings a classic challenge: how to make these pieces talk to each other on the user's screen without duplicating heavy code or making page loading unbearably slow. This is where modern bundling tools come in, a core topic we also explore in advanced strategies on how to render long lists in React maintaining 60 FPS to ensure visual fluidity.

How Module Federation works in practice

For a long time, integrating code from different repositories in the browser meant using clumsy iframes or generating static packages that required reinstalling every minor change. Module Federation changes this game rule by allowing an application to act both as a host (the main container) and as a remote provider that delivers pieces of code on demand.

In simple terms, think of it as a delivery system where the main app doesn't keep all furniture in stock; it simply orders the sofa or table when the customer opens the corresponding room. In practice, the browser downloads the remote module's code only when the user navigates to that specific section, saving internet bandwidth and speeding up initial page opening.

This dynamic exchange happens thanks to native modules in the modern JavaScript ecosystem, which know how to negotiate dependencies at runtime. Thus, if the main module and the remote module use the exact same version of React, the browser downloads the library only once, preventing RAM waste on the user's computer.

Configuring Vite to share code across projects

Vite has become a developer favorite due to its blazing-fast development speed, relying on native browser ES modules instead of bundling everything before startup. To combine this speed with Module Federation, we use specific plugins like @originjs/vite-plugin-federation, which translates sharing concepts into Vite's fast ecosystem.

To get your hands dirty, the first step is configuring the application that will provide the code (the remote microservice) by exposing its features through the bundler configuration file. See below how to structure the vite.config.js file in the remote project:

import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';
import federation from '@originjs/vite-plugin-federation';

export default defineConfig({
  plugins: [
    react(),
    federation({
      name: 'remoteApp',
      filename: 'remoteEntry.js',
      exposes: {
        './Widget': './src/components/Widget.jsx',
      },
      shared: ['react', 'react-dom'],
    }),
  ],
  build: {
    target: 'esnext',
    minify: false,
    cssCodeSplit: false,
  },
});

This code block tells Vite that the component located at src/components/Widget.jsx should be bundled in isolation and made available under the name ./Widget for any external application asking for it. The shared property ensures that React and React-dom are treated as singleton dependencies, meaning they are loaded only once in the global page memory.

Consuming the remote module in the main application

With the remote component published and accessible via network, the next step is configuring the host application to consume this piece of code as if it were in the same local folder. In practice, this turns external components into native pieces of your visual element tree.

Below is an example configuration in the main project (host) to import the remote component exposed in the previous step:

import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';
import federation from '@originjs/vite-plugin-federation';

export default defineConfig({
  plugins: [
    react(),
    federation({
      name: 'hostApp',
      remotes: {
        remoteApp: 'http://localhost:5001/assets/remoteEntry.js',
      },
      shared: ['react', 'react-dom'],
    }),
  ],
  build: {
    target: 'esnext',
    minify: false,
  },
});

With this bridge established in the configuration file, you just need to import the component inside the main application's JavaScript or JSX code using an asynchronous call or proper error boundaries to handle potential network drops:

import React, { lazy, Suspense } from 'react';

const RemoteWidget = lazy(() => import('remoteApp/Widget'));

export default function App() {
  return (
    <div>
      <h1>Main Dashboard<h1>
      <Suspense fallback={<p>Loading widget...<p>}>
        <RemoteWidget />
      <Suspense>
    <div>
  );
}

Using the lazy component combined with Suspense is essential because the browser needs a few milliseconds to download the remoteEntry.js file over the network. While the download happens, the interface displays a friendly loading message instead of freezing the screen.

Managing dependencies and avoiding version conflicts

Sharing dependencies between different teams sounds simple on paper, but requires strict discipline to avoid the nightmare known as version conflict. In practice, if team A updates React to the latest version and team B keeps using an old version, the browser may refuse to execute the code or cause bizarre interface behaviors.

To mitigate this risk, establish clear semantic versioning policies and enforce strict version locking in the package.json file of all micro-frontends. Furthermore, when the ecosystem requires backend databases or complex contracts behind the scenes, it pays off to apply consistent patterns, parallel to the care we discuss in how to decouple databases in legacy microservices.

Another operational best practice is monitoring performance and loading latency of remote files using observability tools and infrastructure metrics, ensuring that a peripheral server outage does not disable the entire main system. To explore these architectural concepts further in production, see our related technical analysis on Slow database? How to speed up queries with partial indexes.

Final thoughts on decentralized architectures

Adopting micro-frontends with Module Federation and Vite is not a silver bullet that solves cultural organization problems overnight; it is a powerful engineering tool for companies already facing real scaling bottlenecks in front-end monoliths. Operational autonomy gains momentum, allowing different teams to deliver value independently and continuously.

In short, the success of this architecture depends as much on choosing the right tools as on rigor in integration contracts and shared dependency governance. Plan your business domain division well before writing the first line of code, keep communication between teams active, and enjoy the agility that modern modularity brings to your web product.