Mastering Microfrontends: Sharing Dependencies Across Apps
Previously we saw how we can build our single-page application using microfrontends in my previous blog post. In this post, we will explore how dependencies work within a microfrontend host and remote apps. We will look at how packages get loaded with Webpack Module Federation, how we can share the same versions of packages, and how we can use specific versions of each package in our host and remote apps.
With microfrontends, there is the ability to run each remote app in isolation and also as part of the host app. Running in isolation has the benefit of allowing each team to easily develop and test their remote app before integrating it with the rest of the application within the host app.
When each app is run in isolation it is no different than any monolithic SPA that we are familiar with. Further inspecting the Network tab in the browser debugger tools, we can see each application’s code is bundled into a main.js file and each dependency (@faker-js/faker, react, react-dom) is being loaded individually.
As discrete standalone applications, this behaviour is expected and nothing out of the ordinary. But what does this look like when we inspect the host app?
When we take a closer look at the host app, we see that it is also loading two identical copies of @faker-js/faker (remember we have been using the same version in each remote app). Why is this happening? Let’s explore in the next section.
The source code for this project can be found in my GitHub repository here.
How The Host App Loads Codes
To understand why this is happening we should first review how the Webpack Module Federation plugin is fetching code from each app.
The host app contains the remotes entries for each of the products
and cart
remote apps. This is the file that further instructs Webpack where to fetch the code for each of the remote apps.
This is performed in a sequence of steps:
- The
host
app fetches theproducts
remoteEntry.js file - The remoteEntry.js file requires @faker-js/faker so it is fetched
- The
host
app fetches thecart
remoteEntry.js file - Again, the remoteEntry.js file requires @faker-js/faker so it is fetched once more
As a result, we end up seeing that the host
app ends up fetching two identical copies of @faker-js/faker. But how come it does not fetch two copies of react or even react-dom? And how can we only fetch one copy of @faker-js/faker?
Sharing Dependencies Between Apps
The answer to both of the previous questions can be answered in one configuration within the Webpack Module Federation plugin configuration.
When we used the create-mf-app
template to generate our microfrontend apps, it scaffolds a lot of code in the webpack.config.js file. The Webpack Module Federation plugin configuration includes a shared section that allows us to define which dependencies should be shared across apps.
By default, this template shares all the dependencies in the package.json file. The only exception is the react and react-dom libraries that are configured to be singletons in order to prevent multiple copies of react and react-dom to be loaded in the browser which would cause all sorts of errors.
Now to allow us to share only one instance of @faker-js/faker, we can simply add the devDepdencies
from package.json to this list.
Now if we restart the products
and cart
remote apps and re-load the host app we can see that there is indeed only one copy of @faker-js/faker being loaded now. As a result, the overall package size of the JavaScript files being loaded has been reduced from 11.8 MB to 7.5 MB (a 36% improvement before even minifying our code)!
Specifying Versions in Remote Apps
Within our microfrontend application, each of the remote apps may be managed by different teams. As we have seen before, each of these teams may prefer or have a reason to use different technologies or even different package versions.
Suppose the team that is responsible for the cart
remote app is using an older version of @faker-js/faker, suppose version 6.3.1 instead of 7.4.0 in products
. We no longer want to share all the dependencies in the package.json file or Webpack will by default use the higher version available.
To do this we can remove the devDependencies from the webpack.config.js in each of our remote apps. This tells the Webpack Module Federation plugin not to load one copy of the dependencies for all of the remote apps.
Once we restart our apps, we can inspect again to see that we are indeed getting two different versions of @faker-js/faker for each of the remote apps. Of course, our bundle size naturally has also increased from 7.5 MB to 10.5 MB. This is to be expected because we require multiple copies of the package to be loaded.
Wrapping Up
Now that we have seen how the Webpack Module Federation plugin helps manage package dependencies within a microfrontend app, the next series of posts will explore how to handle communication within a microfrontend application.
References
Microfrontends with React: A Complete Developer’s Guide
https://www.udemy.com/course/microfrontend-course/
Eric Bach is a Senior Software Developer @ Alberta Motor Association who enjoys learning, reading, and writing about leadership principles, event-driven microservices, and all things AWS.