We use cookies to personalize your experience.Learn More

Blog / Engineering

Using Webpack Module Federation for Micro-Frontends

Webpack Module Federation allows dynamic loading of modules from external sources on demand. This helps build micro-frontends without all the baggage. Let's dive in!

Soumik Chaudhuri

Thu Aug 10 20238 min read

Module Federation is a highly advantageous feature of Webpack Version 5, offering unparalleled versatility and adaptability to applications. Introducing the groundbreaking concept of code sharing among applications, it empowers users to partition the logic of their applications into multiple sub-applications.

This not only provides an independent Javascript runtime for each application but also allows deploying changes very easily. In this blog post, we will explore the concepts revolving around Module Federation and why you should integrate it into your own application system, and a deep dive into how our product Canonic uses it.


What are micro-frontends and why do we need them?

Traditionally, when building a web application, the frontend logic would reside within a monolithic codebase, where all the components and functionalities were tightly coupled together. However, as applications grow larger and more complex, maintaining and scaling such monolithic architectures becomes challenging. You may be familiar with the idea of microservices, where the backend logic of an application is divided into smaller components. For example, an authentication system or an email service can be a microservice as they have specific functionality.

Introduction

Webpack Module Federation has transformed the way we design JavaScript applications and implement micro-frontend approaches. To overcome the challenges of maintaining large monolithic frontend repositories, the concept of micro-frontends emerged. Like microservices on the backend, micro-frontends allow the frontend codebase to be divided into smaller, more manageable parts. Each micro-frontend represents a self-contained, independent sub-application responsible for a specific feature or functionality. By breaking down the front end into micro-frontends, development teams can work on different parts of the application in parallel. This fosters better separation of concerns and allows teams to focus on their respective areas of expertise.

How Module Federation helps in micro-frontend architecture

In the past, sharing code between different parts of an application was often a challenging task and the implementation of micro-frontends was also becoming increasingly complicated. However, with the introduction of Module Federation, these issues have been addressed, and the micro-frontend strategy has undergone a revolution. Module Federation allows each micro-frontend to dynamically load and run its own code, as well as consume functionality from other micro-frontends during runtime.

This enhances modularity, making it easier to add or remove features without affecting the entire application. It also enables independent deployment of micro-frontends, allowing teams to release updates and bug fixes without impacting the entire application. Additionally, it promotes scalability, as micro-frontends can be developed, tested, and deployed independently, enabling better resource allocation and team collaboration.


What is dependency sharing?

Another extremely useful feature of Module Federations is dependency sharing. It refers to the capability of sharing common dependencies, such as modules or libraries, among multiple applications within a Webpack setup. It allows applications to leverage and reuse shared dependencies, eliminating the need for redundant code and reducing the overall size of the bundled output. By efficiently managing dependencies, Module Federations promote modular development, enhance code reusability, and improve the overall efficiency and scalability of the application ecosystem.

Efficient Sharing

Module Federation optimizes the sharing of dependencies between applications in an efficient manner. It intelligently analyzes the dependency graph and determines the most efficient way to share common modules. This minimizes duplication and reduces the overall size of the bundled code. When multiple applications share the same dependencies, Module Federation handles version compatibility as well. It ensures that each application uses the appropriate version of a shared module, preventing conflicts and maintaining a consistent runtime environment.

Dynamic Updates

Module Federation also supports dynamic updates of shared dependencies. When a shared module is updated or a new version is released, Module Federation allows applications to seamlessly consume the updated module without requiring a full application reload. This ensures that applications can quickly adopt improvements or bug fixes in shared dependencies.

Independent Deployment

With Module Federation, shared dependencies can be independently deployed and versioned. This means that different teams or projects can develop and release updates to shared modules at their own pace, without tightly coupling them to the applications that consume them. It promotes a modular and decoupled development workflow.

Granular Control

Module Federation offers granular control over shared dependencies. Developers can specify which modules are shared and which ones are private to each application. This allows for fine-grained management of dependencies and provides flexibility in configuring the sharing behavior based on specific application requirements.


How are deployments and performance of applications affected?

Module Federation plays a pivotal role in deployments and performance enhancement of applications. By enabling the dynamic loading of code and sharing of dependencies, it allows for independent deployment of application modules. This means that updates or changes to specific parts of the application can be deployed without impacting the entire system, resulting in faster and more efficient deployments. Additionally, by sharing dependencies and loading code on-demand, Module Federation optimizes the performance of applications by reducing initial loading times and improving resource utilization, resulting in a smoother and more responsive user experience.

Incremental Updates

With Module Federation, updates to individual modules or sub-applications can be deployed independently. This incremental update approach reduces the deployment time and minimizes downtime for the entire application. It also allows for faster iteration and more agile development processes.

Smaller Bundle Size

Module Federation allows for the sharing of common dependencies between applications. This reduces the duplication of code and significantly decreases the initial bundle size of each application. As a result, users experience faster loading times and improved performance when accessing the application.

Scalability and Maintainability

Each module can be developed, tested, and deployed independently, allowing for better resource allocation and easier management of the overall application.


How Canonic uses Webpack Module Federation

Now that we have seen the various benefits provided by Module Federation, including code splitting, dependency sharing, performance benefits, and so on, let us try to understand how we use it for Canonic. Since Canonic is a low-code platform including various integrations and support for UI libraries, it is important to separate frontend logic into smaller sub-applications. This not only benefits our performance speeds but also helps us manage the deployment cycle and code management of our application.

UI Libraries and Compositions

Canonic provides support for UI Libraries and their compositions like Material UI. This feature allows users to use Material UI components such as Paper, Buttons, Inputs, etc as well as customizable compositions like cards with statistics, ticket boards, and Gantt charts to name a few, with just a drag and drop.

Challenges we faced

Considering the number of components and compositions that can be supported, including all of them in the main application repository poses several challenges -

  • Making a small change in any one of the components will require a redeployment of the entire application repository.
  • Load times will increase when it comes to loading the components and compositions dynamically. This also affects the overall performance of the web application.
  • When different micro-frontends are developed separately, there might be redundant copies of shared dependencies (like React, lodash, etc.) across the bundles, leading to increased bundle sizes and potential version conflicts.
  • When using multiple UI libraries or compositions from different teams, ensuring a consistent API and communication mechanism between them is a hassle.

Solutions to our problems

  • With Module Federation, each micro-frontend (UI library or composition) is developed and deployed independently. This allows us to work on their respective components without affecting the others.
  • Module Federation enables dynamic loading of micro-frontends on-demand. This means that components are fetched and loaded only when required, reducing the initial bundle size and improving load times.
  • Module Federation allows you to share dependencies between micro-frontends. Instead of bundling shared dependencies with each micro-frontend, they can be provided from a centralized location (the remote) and used by other micro-frontends.
  • With Module Federation, we can explicitly define the shared APIs and communication contracts between micro-frontends. This helps enforce consistency and provides a structured way for micro-frontends to interact with each other, ensuring smooth integration.

Thus, Module Federation provided by Webpack serves as the perfect solution to all these challenges. The main application repository named canonic-app can dynamically load a smaller bundle for the UI libraries and compositions which is the canonic-ui repository. On top of this, the dependencies are shared amongst both repositories, which provides all the advantages that were discussed before.


How to use Module Federation to build micro-frontends

Let's take a quick look at how you can use Webpack Module Federation to build a simple micro-frontend architecture. In this tutorial, we will have two applications, a host application, and a remote application.

From the host, the rest of the application's components and modules are loaded and executed. The remote application refers to an additional Webpack build that the host application can utilize to access specific parts.

First, let's set up two React projects, which are going to act as the host application and the remote application. Additionally, we need to install some dependencies.

mkdir host 
cd host 
npx create-react-app .
yarn add webpack webpack-cli webpack-server html-webpack-plugin babel-loader webpack-dev-server css-loader

mkdir remote 
cd remote
npx create-react-app .
yarn add webpack webpack-cli webpack-server html-webpack-plugin babel-loader webpack-dev-server

Now, in the remote application, create a new file in the src folder called Button.js and copy this code into it.

import React from "react";

const Button = ({ handleClick, text }) => {
  return <button onClick={handleClick}>Hey {text}</button>;
};

export default Button;

Also, update your App.js file in the remote application with the following code. This will help us distinguish the UI of the remote application from that of the host application.

import Button from "./Button";

function App() {
  return (
    <div className="App">
      <h1>Remote application</h1>
      <Button text="Remote application" />
    </div>
  );
}

export default App;

Now create the webpack.config.js file in the root folder of your remote application and copy this code into it. This file uses the ModuleFederationPlugin and the exposes configuration option to expose the Button component.

const HtmlWebpackPlugin = require("html-webpack-plugin");
const ModuleFederationPlugin = require("webpack/lib/container/ModuleFederationPlugin");
module.exports = {
  mode: "development",
  devServer: {
    port: 8083,
  },
  module: {
    rules: [
      {
        test: /\.js?$/,
        exclude: /node_modules/,
        // To Use babel Loader
        loader: "babel-loader",
        options: {
          presets: [
            "@babel/preset-env" /* to transfer any advansed ES to ES5 */,
            "@babel/preset-react",
          ], // to compile react to ES5
        },
      },
    ],
  },
  plugins: [
    new ModuleFederationPlugin({
      name: "remote",
      filename: "remoteEntry.js",
      exposes: {
        "./Button": "./src/Button",
      },
    }),
    new HtmlWebpackPlugin({
      template: "./public/index.html",
    }),
  ],
};

Similarly, in the host application, create a webpack.config.js file in the root directory and copy this code into it. It uses the same plugin to establish the remote application so that it can import the Button component.

const HtmlWebpackPlugin = require("html-webpack-plugin");
const ModuleFederationPlugin = require("webpack/lib/container/ModuleFederationPlugin");
module.exports = {
  mode: "development",
  devServer: {
    port: 8082,
  },
  module: {
    rules: [
      {
        test: /\.js?$/,
        exclude: /node_modules/,
        // To Use babel Loader
        loader: "babel-loader",
        options: {
          presets: [
            "@babel/preset-env" /* to transfer any advansed ES to ES5 */,
            "@babel/preset-react",
          ], // to compile react to ES5
        },
      },
    ],
  },
  plugins: [
    new ModuleFederationPlugin({
      name: "host",
      filename: "remoteEntry.js",
      remotes: {
        remote: "remote@http://localhost:8083/remoteEntry.js",
      },
    }),
    new HtmlWebpackPlugin({
      template: "./public/index.html",
    }),
  ],
};

Now, in the App.js file of the host application, copy this code to import the Button component and use it.

import React from "react";
import Button from "remote/Button";

function App() {
  return (
    <div className="App">
      <h1>Host application</h1>
      <Button text="Host application" />
    </div>
  );
}

export default App;

Finally, rename the index.js file in the src folder in both the host and remote applications to wrapper.js. Create another file called index.js and paste this code.

import("./wrapper");

Now we are ready to run both applications. Run the following command in the terminal for the host and the remote.

yarn webpack serve

And voila! We have our remote Button component in the host application.


Get started with Canonic

Canonic is an amazing low-code platform that lets you create sophisticated internal tools, interfaces, and automation with ease. Its user-friendly drag-and-drop feature allows you to build impressive interfaces effortlessly. Plus, it supports popular libraries like Material UI for front-end development. With the help of Module Federation via Webpack, Canonic seamlessly integrates a wide range of components and compositions, making the interface-building process a breeze.

Get Started Today 🚀

Did you find what you were looking for?
👍
👎
What went wrong?
Want to discuss?We have a thriving Discordcommunity that can help you. →

Enough said, let's start building

Start using canonic's fullstack solution to build internal tools for free