Micro Frontends

Micro-Frontends means small, standalone and deliverable frontend applications combined to form a whole single application. The idea is the same as micro-services architecture for backend where we develop small API services. A Micro-Frontend is a portion of a webpage (not the entire page).

Micro Frontends

What are Micro Frontends?

Micro-Frontends means small, standalone and deliverable frontend applications combined to form a whole single application. The idea is the same as micro-services architecture for backend where we develop small API services. A Micro-Frontend is a portion of a webpage (not the entire page). In the Micro-Frontend Architecture, there is a "Host" or a "Container" page that can host one or more Micro-Frontends. The Host/Container page can also share some of its own Micro-Frontend components.

Why Use Micro Frontends?

The Architecture we currently use is Monolithic, it means there is one repository, one team developing the entire frontend working on the same code. As the application grows (which most of the applications do with time) the  development, testing and maintainability becomes difficult. To solve these problems we can use Micro-Frontend Architecture. We divide a team working on a single application into small independent teams who can choose their own tech stack and can do development however they like and then we deploy the frontends independently.

Some of the key benefits of Micro-Frontends are:

  • Smaller, more cohesive and maintainable codebases.
  • More scalable organizations with decoupled, autonomous teams.
  • The ability to upgrade, update, or even rewrite parts of the frontend in a more incremental fashion than was previously possible.

Trade Offs

A downside of Micro-Frontends is that there isn't a single implementation, there are many ways of achieving independent deployments and architectures vary depending on the company applying them. This can cause confusion and a steeper learning curve for developers trying to adopt this architecture.

There will be code duplication so different developers might be spending time on writing the same code because of teams working independently.

When to use Micro Frontends?

When you have a large application or you know that the application will grow in size in future then investing in micro frontends is worth it but if the application size will be small then using monolithic architecture is better.

How to Integrate Micro Frontends

There are two strategies to Integrate Micro-Frontends

1. Build Time Integration

2. Run Time Integration

Build Time Integration

Micro Frontends can be integrated at build time by being treated as Modules as npm packages. They can be imported into container and can be used just like any other package.

The issues with this approach are:

  • Syncing different versions of libraries and build issues.
  • It is very hard to use different technologies.
  • The size of the final package will be big because it contained all dependencies.
  • Will have to deploy again for any changes in dependencies.
  • Tight coupling between the container and all Micro-Frontends.

Run Time Integration

Micro frontends can be integrated at runtime also. As a frontend consists of html, CSS and JavaScript files which can use external assets these files can be directly used inside the container html file at run time. We can decide which file (which will be our micro frontend) to use at a specific location. There are two ways of doing it

  • Server-Side Composition.
  • Client Side Composition.

Server Side Composition

As the name suggests this one involves a server. Html pages are created by conditional usage of micro frontends and using them as fragments on the server and then are served to the client. As html does not allow direct conditional statements we can use some other scripting or markup language like

  • Server Side Includes (SSI)
  • Edge Side Includes (ESI)

Client Side Composition

In client side composition we provide the urls for micro frontends inside the container html in the the following ways to integrate micro frontends

  • Via Web Components
  • Via iframe
  • via JavaScript

Frameworks

  • Webpack 5 Module Federation
  • Bit
  • Luigi
  • Single Spa
  • Systemjs

We will discuss working of only one of the above frameworks

Webpack Module Federation

It is a webpack feature that allows us to use components from remote locations. We can develop standalone frontend applications and deploy them independently and then use them inside one container using this framework.

Here is the standard webpack configuration

const path = require("path");
const HtmlWebpackPlugin = require("html-webpack-plugin");
 
module.exports = {
    entry: "./src/index.js",
    output: {
        filename: "main.js",
        path: path.resolve(__dirname, "build"),
    },
    devServer: {
        static: {
 directory: path.join(__dirname, 'public'),
        },
        compress: true,
        port: 3001,
        historyApiFallback: true,
    },
    plugins: [
        new HtmlWebpackPlugin({
            template: path.join(__dirname, "public", "index.html"),
        }),
    module: {
        rules: [
            {
                test: /\.(js|jsx)$/,
                exclude: /node_modules/,
                use: ["babel-loader"],
            },
        ],
    },
    resolve: {
        extensions: ["*", ".js", ".jsx"],
    }
};

We just need to use the module federation plugin which can be imported from webpack. This configuration is for a micro frontend exposing the whole app for a remote container to use. We will need to deploy it separately.

const path = require("path");
const HtmlWebpackPlugin = require("html-webpack-plugin");
const ModuleFederationPlugin =
                require("webpack/lib/container/ModuleFederationPlugin");
 
module.exports = {
    entry: "./src/index.js",
    output: {
        filename: "main.js",
        path: path.resolve(__dirname, "build"),
    },
    devServer: {
        static: {
            directory: path.join(__dirname, 'public'),
        },
        compress: true,
        port: 3001,
        historyApiFallback: true,
    },
    plugins: [
        new HtmlWebpackPlugin({
            template: path.join(__dirname, "public", "index.html"),
        }),
        new ModuleFederationPlugin({
            name: "frontend1",
            filename: "remoteEntry.js",
            exposes: {
                "./Frontend1App": "./src/App",
            },
        }),
    ],
    module: {
        rules: [
            {
                test: /\.(js|jsx)$/,
                exclude: /node_modules/,
                use: ["babel-loader"],
            },
        ],
    },
    resolve: {
        extensions: ["*", ".js", ".jsx"],
    }
};

And then container can use the exposed app inside them using the above apps url as a remote service. Here is how we do it in code

const path = require("path");
const HtmlWebpackPlugin = require("html-webpack-plugin");
const ModuleFederationPlugin = require("webpack/lib/container/ModuleFederationPlugin");
 
module.exports = {
  entry: "./src/index.js",
  output: {
    filename: "main.js",
    path: path.resolve(__dirname, "build"),
  },
  devServer: {
    static: {
      directory: path.join(__dirname, 'public'),
    },
    compress: true,
    port: 3000,
    historyApiFallback: true,
  },
  plugins: [
    new HtmlWebpackPlugin({
      template: path.join(__dirname, "public", "index.html"),
    }),
    new ModuleFederationPlugin({
      name: "container",
      remotes: {
        frontend1: '[email protected]://localhost:3001/remoteEntry.js',
        frontend2: '[email protected]://localhost:3002/remoteEntry.js',
      },
    }),
  ],
  module: {
    rules: [
      {
        test: /\.(js|jsx)$/,
        exclude: /node_modules/,
        use: ["babel-loader"],
      },
    ],
  },
  resolve: {
    extensions: ["*", ".js", ".jsx"],
  }
};

Now that app is available to us at location frontend1/Frontend1App where frontend1 is the name we specified in container when using the remote url and /Frontend1App is the location we specified when exposing frontend1.
We can use React.Lazy to load the remote frontend asynchronously and when needed and with React.Suspense we can render the remote frontend in our container.

import React, { Suspense } from 'react'
const FrontendApp1 = React.lazy(() => import('frontend1/Frontend1App'));
const FrontendApp2 = React.lazy(() => import('frontend2/FrontendApp'));
import { BrowserRouter as Router, Routes, Route } from 'react-router-dom';
 
const App = () => {
 
  return (
    <Router>
      <Routes>
        <Route path='/' element={<h1>Container</h1>} />
        <Route 
 		path='/frontend1/*' 
           element={
       <Suspense fallback={'loading'}>
          		<FrontendApp1 />
        	 </Suspense>}
        >
        </Route>
        <Route path='/frontend2' element={<Suspense fallback={'loading'}>
          <FrontendApp2 />
        </Suspense>}/>
      </Routes>
    </Router>
  )
}
 
export default App

Note: Remote frontends can share the information about their dependencies i.e what version of a certain dependency is being used in a certain frontend, by using shared option.

Summary

Micro Frontends solve the problem of large code base (which is more prone to bugs) by dividing the whole application into smaller apps.

Should only be used when:

  • The application scope is large enough that it can be divided into smaller apps, and should not be used for smaller applications because it would be a waste of resources.
  • All the smaller applications should be independent and should be able to exist without any parent.
  • We can use Webpack Module Federation for Integrating Micro-Frontends into a container application.


What are your thoughts? You are open to ask any Question and leave a comment. Thanks