1. The Backstory: The Monolith React App That Stalled Developer Velocity

At CodexiLab, we frequently consult with scale-up startups on frontend infrastructure optimization. Last year, a fintech platform client came to us with a major bottleneck in their delivery pipeline. Their primary product—a multi-module trading and analytics dashboard—had been built as a single, massive React monolith. Over four years, the codebase had grown to over 500,000 lines of code, containing complex chart engines, administrative consoles, and user billing screens.

Because of this massive size, their local development hot-reload loop took upwards of 12 seconds, and their CI/CD build pipeline took a painful 18 minutes. If an engineer wanted to deploy a simple typo fix on the billing page, they had to rebuild the entire application, run the full test suite, and deploy the entire monolith. This created a high risk of deployment blockages and slowed the engineering team's iteration speed to a crawl. We decided to split the monolith into independent, decoupled micro-frontends using Vite and Module Federation. This guide details the architecture, configuration parameters, and styling isolation techniques we used to restore a 20-second CI build loop and enable independent team deployments.

2. Understanding Micro-Frontends: Host Apps vs. Remote Apps

Micro-frontends apply the principles of microservices to the frontend. Instead of building a single application, the interface is split into a set of independent, loosely coupled widgets that are assembled at runtime. In this architecture, we define two types of applications:

  • The Host (Shell): The main container application that handles global concerns like user authentication, layout structure, header navigation, and theme configurations. It dynamically imports and renders individual widgets from other services.
  • The Remotes (Micro-apps): Independent applications focused on specific features (e.g. billing-remote, analytics-remote). These remotes are built and deployed to their own servers (e.g., S3/Cloudfront or Vercel). They export specific components or sub-routes that the host can import and execute on the fly.

Historically, Webpack Module Federation was the standard tool for micro-frontends. However, Webpack is slow. We wanted to leverage the speed of Vite, which uses native ES Modules (ESM) and esbuild under the hood. To make Module Federation work with Vite, we use the @originjs/vite-plugin-federation plugin. This plugin enables compiling and loading remote micro-apps using standard ES Modules, maintaining Vite's ultra-fast local development experience.

3. The Danger of Shared Dependencies: Preventing Version Mismatches

The biggest challenge in micro-frontend architectures is dependency management. If both your Host application and your Billing Remote application use React, you do not want the browser to download two independent copies of the React library. Doing so would double your bundle size, degrade page load performance, and create runtime errors because React expects a single global instance of its virtual DOM to be active.

To solve this, Module Federation allows us to define 'Shared Dependencies'. In our Vite configurations, we specify that libraries like react, react-dom, and react-router-dom are shared. When the Host application loads, it downloads React. When it later loads the Billing Remote, the remote checks the Host's environment, sees that React is already loaded, and reuses the existing instance.

However, what happens if the Host uses React 18.2, but the Billing Remote is upgraded to React 19.0? If the versions are incompatible, the application will crash. To prevent this, we configure 'Shared Version Rules' using semver ranges. We can set React as a singleton dependency, forcing both host and remotes to use the exact same version, or fallback gracefully if versions are mismatched.

javascript
// vite.config.ts for the Host (Shell) Application
import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';
import federation from '@originjs/vite-plugin-federation';

export default defineConfig({
  plugins: [
    react(),
    federation({
      name: 'host_app',
      remotes: {
        // Define remote entry points pointing to independent build locations
        billing_app: 'http://localhost:5001/assets/remoteEntry.js',
        analytics_app: 'http://localhost:5002/assets/remoteEntry.js'
      },
      shared: {
        // Enforce React as a singleton shared dependency
        react: {
          singleton: true,
          requiredVersion: '^18.2.0'
        },
        'react-dom': {
          singleton: true,
          requiredVersion: '^18.2.0'
        }
      }
    })
  ],
  build: {
    modulePreload: false,
    target: 'esnext',
    minify: false,
    cssCodeSplit: false
  }
});

4. Step-by-Step Configuration of the Remote App

The code block above illustrates the Host's configuration. To complete the link, we must configure the Remote application (e.g. billing_app) to export its components. The remote configuration specifies its name, the filename of its entry point (typically remoteEntry.js), and a map of exported files (exposed components). Below is the Vite configuration for the Billing Remote application.

javascript
// vite.config.ts for the Billing Remote Application
import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';
import federation from '@originjs/vite-plugin-federation';

export default defineConfig({
  plugins: [
    react(),
    federation({
      name: 'billing_app',
      filename: 'remoteEntry.js',
      // Export specific components to be loaded by the host
      exposes: {
        './BillingDashboard': './src/components/BillingDashboard.tsx',
        './PaymentForm': './src/components/PaymentForm.tsx'
      },
      shared: {
        react: {
          singleton: true,
          requiredVersion: '^18.2.0'
        },
        'react-dom': {
          singleton: true,
          requiredVersion: '^18.2.0'
        }
      }
    })
  ],
  build: {
    modulePreload: false,
    target: 'esnext',
    minify: false,
    cssCodeSplit: false
  }
});

5. CSS Isolation: Preventing Style Bleeding Across Remotes

In a micro-frontend architecture, style bleeding is a major headache. If the Billing Remote has a CSS class named .btn { background: red; }, and the Host or another remote has a class named .btn { background: blue; }, the styles will conflict in the browser. Whichever CSS file is loaded last will overwrite the previous styles, causing UI visual bugs across the entire application.

To solve this, we enforce three design system rules at CodexiLab:

  1. CSS Modules: All component-specific styles must use CSS Modules (e.g. Billing.module.css). Vite automatically compiles these modules by appending a unique hash to every class name (e.g., _btn_1z8a9_5), ensuring that classes never conflict.

  2. Tailwind Prefixing: If the team uses TailwindCSS, we configure a custom prefix inside the Tailwind config of each remote (e.g., prefix: 'billing-'). This namespace ensures that a utility class like billing-flex never conflicts with standard flex styles on the host.

  3. Shadow DOM Encapsulation: For legacy widgets where CSS code cannot be refactored, we wrap the remote component inside a Shadow DOM container. The Shadow DOM creates a boundary that prevents style sheets declared outside from affecting the elements inside, guaranteeing total visual isolation.

6. The Results: Sub-Minute Builds and Unleashed Developers

Splitting our client's monolithic React application into Vite micro-frontends had a massive impact on their operations. Their main CI/CD build pipeline execution time plummeted from 18 minutes to just 42 seconds for independent remote deploys. Local development hot-reload loops returned to sub-second responses because developers only compile the specific micro-app they are actively coding.

Most importantly, different engineering teams can now work and deploy independently. The billing team can ship upgrades to the payment flows three times a day without requiring coordination or sync meetings with the core platform team, accelerating their product delivery speed and reducing operational risks.

7. Summary: When to Adopt Micro-Frontends

Micro-frontends are an exceptional pattern for large organizations with multiple development teams working on a single digital product. By leveraging Vite and Module Federation, you can maintain build speeds, isolate deployment risks, and keep teams decoupled. However, for small startups with a single development team, the overhead of managing multiple configurations and build pipelines is rarely worth it; stick to a clean, modular monolith until scale demands a split.

8. Frequently Asked Questions (FAQ)

Q: How do we share user authentication tokens between the host and remote apps?
A: Authentication tokens should be stored in a shared location, such as local storage or a secure cookie scoped to your root domain (e.g. *.codexilab.com). The Host application reads the token, and the remotes pull it from the shared storage when executing API calls.

Q: Can we load Webpack-built remotes into a Vite host?
A: Yes, but it requires using a compatibility plugin like @originjs/vite-plugin-federation's webpack integration. Because Webpack and Vite compile assets differently, there can be edge-case bugs, so we recommend using Vite for both host and remotes if possible.

Q: How do we handle routing in a micro-frontend setup?
A: The Host application should define the parent routing structure (e.g. /billing/* routing to the Billing Remote). The remote application then uses sub-routes relative to its mount path, allowing it to control its internal page navigation independently.