Sharing Is Caring – Boosting Micro-frontends Performance With Dependency Sharing

This ad is not shown to multipass and full ticket holders
React Summit US
React Summit US 2025
November 18 - 21, 2025
New York, US & Online
The biggest React conference in the US
Learn More
In partnership with Focus Reactive
Upcoming event
React Summit US 2025
React Summit US 2025
November 18 - 21, 2025. New York, US & Online
Learn more
Bookmark
Rate this content

Join me as we take a look into the dark side of micro-frontends - dependency duplication. Ever wondered what happens to your overall bundle size when you break a huge SPA into dozens of micro-frontends? In this session we’ll share our journey with sharing dependencies between micro-frontends in monday.com to reduce overall bundle size on the page and increase performance.

This talk has been presented at JSNation 2025, check out the latest edition of this JavaScript Conference.

FAQ

Monday.com addresses the bundle size issue by sharing dependencies between micro-frontends, ensuring that only one instance of a dependency is loaded per major version, and managing these shared dependencies through a spec and metadata files.

The 'hot swap solution' involves excluding dependencies from micro-frontend bundles and creating a dedicated bundle per dependency. When a micro-frontend loads, dependencies are checked and loaded if needed, with the possibility to 'hot swap' versions to meet specific requirements.

Monday.com ensures compatibility by providing each micro-frontend with the exact version of a dependency it requires, and when multiple micro-frontends use the same major version, using compound names to manage sub-dependencies and avoid conflicts.

The global dependencies solution involves maintaining a global metadata file that records which micro-frontend has the highest version of each dependency, simplifying the runtime process by loading shared dependencies from this global reference.

Monday.com handles implicit dependencies by using the 'require.resolve' function instead of relying solely on the project's package.json, ensuring all necessary dependencies are included and shared effectively.

Implementing shared dependencies at monday.com saved around 10 megabytes of overall bundle size per page, significantly boosting performance metrics.

Monday.com implemented kill switches and safety mechanisms, rolled out the solution experimentally and gradually over six months, and ensured compatibility with Sanver to prevent production breakage.

Yes, the effort was worth it as it resulted in significant performance improvements and reduction in bundle size, enhancing overall user experience.

The main challenge of using micro-frontends is managing bundle size, as each micro-frontend may bundle shared dependencies like React, leading to increased load times and memory usage, negatively affecting performance.

Micro-frontends are a front-end architectural pattern where a single-page application is split into smaller, independently maintained and deployed applications, allowing for better team independence, system scoping, and isolation.

Sahar Brodbeker
Sahar Brodbeker
22 min
16 Jun, 2025

Comments

Sign in or register to post your comment.
Video Summary and Transcription
Saar Becker discusses the challenges of micro-frontends at monday.com, focusing on bundle size due to loading React multiple times. The hot swap solution v1 is introduced to address this issue by sharing dependencies efficiently. Strategies include providing the same React version, managing shared dependencies through hot swaps, and optimizing bundle building by creating compound-named bundles. The talk delves into revising hot-swapping solutions, resolving indirect dependency issues, and optimizing dependency bundle building for production readiness.

1. Exploring Micro-Frontends Challenges

Short description:

Saar Becker walks through monday.com's micro-frontends sharing dependencies journey. Quick refresher on micro-frontends: splitting one big single-page app into smaller apps. Challenges include bundle size due to loading React multiple times.

Hi, everyone. My name is Saar Becker, and today I'm going to walk you through our journey at monday.com with sharing dependencies between our micro-frontends. We're going to go through all of the challenges that we faced, all of the solutions that we tried, and all of the lessons that we learned along the way. But before we dive into the details of everything, let's have a quick refresher on what micro-frontends are.

So, micro-frontends is a very popular front-end architectural pattern in which we take one big single-page application and split it into smaller applications. Like in this instance, we have service A, service B, and service C, each being individually maintained and deployed to production, and this allows for better independence for teams, better scoping of our system, and better isolation.

But with all of the neat stuff that the front-end architecture gives us, it also presents a few key challenges. Chiefly among them is bundle size. So what you can see here is when we had a single-page application, we loaded React into the page only once, because it existed in our bundle only once. We had one bundle, basically.

2. Addressing Micro-Frontends Bundle Size

Short description:

React loaded multiple times in micro-frontends leads to bigger bundle size and worse performance. Sharing dependencies to improve performance; aiming for compliance and efficient sharing. Initial solution v1 named hot swap solution for better performance in micro-frontends.

Now when we have micro-frontends, React is basically bundled into every single one of our micro-frontends, meaning it's being loaded to the page as many times as the amount of micro-frontends that we have. So in this instance, we load React to the page three times. And roughly speaking, bigger bundle size overall on the page, the more assets we download and the more stuff we load into the memory, results in worse performance. It results in longer network times, it results in a bigger memory footprint of our tabs, and that's bad. So just to get this into perspective with some numbers, when we load three instances of React on the page, we add around 15 kilobytes of code to the page. And if we load, let's say, more micro-frontends with a bigger package, like MobX, for example, we add around 630 kilobytes to the page, which is not negligible at all, and it affects the performance of the tab, like we mentioned.

What can we do about this? As you might have guessed by the name of this talk and the introduction, we can share, because sharing is caring. So we set out to create a spec for how we're going to share dependencies between our micro-frontends to allow for better performance overall in our application. So first and foremost, we want to share as much as possible, going under the assumption that the more things we share, the better the performance is going to be overall. Having said that, we want to make sure that we're compliant with the micro-frontend's demand, basically that we're compliant with December, so that nothing breaks on the page. Yeah, that's a pretty big thing, I'd say. And that includes the two points.

So with that spec in mind, we went out to build our first solution. And we're going to go over what we did in build time and what we did in runtime for all of our solutions. The first iteration of our solution, we call it solution v1. We call it the hot swap solution.

3. Optimizing Shared Dependencies

Short description:

Providing same React version for micro-frontends; avoiding catalog packages. Building the first solution v1, the hot swap solution, excludes dependencies from bundles and uses window objects for references.

But let's say that we have two different micro-frontends that use the same major version of React. Let's say they're both using React 18. We want to be able to provide them with the same version of React, the same instance of React, to save on bundle size. But there's a small caveat to that, and we want to make sure that while we provide React 18 for both of them, that we're not breaking any one of them. So let's say that one is needing React 18.1.0 and the other one needs React 18.3.0. We cannot possibly give the second micro-frontend React 18.1.0, because we then risk the possibility of breaking its runtime code.

And finally, in our spec, we don't want to include any catalog packages. And what we mean by catalog packages is that we don't want to include any dependencies that are in the sense of a very wide library that includes a lot of functions or components. Let's take lodash, for example, where you might have a catalog of a lot of different kinds of functions, but usually what ends up happening is that you use only a small subsection of those functions, and it would be kind of a waste to load the entirety of lodash, because you cannot have tree-shaking with our solution.

So with that spec in mind, we went out to build our first solution. And we're going to go over what we did in build time and what we did in runtime for all of our solutions. The first iteration of our solution, we call it solution v1. We call it the hot swap solution. So how it works, we're going to start with the build process. First and foremost, we want to exclude all of these dependencies from the micro-frontend bundle. So in this instance, we don't want to include React's source code in our bundle. So we make sure that instead of using React and bundling it into our micro-frontend bundle, we create some sort of a reference to a window object of some sort.

4. Managing Micro-Frontend Dependencies

Short description:

Creating bundles per dependency with metadata files for micro-frontends. Managing shared dependencies, including hot swaps for compatibility. Dealing with sub-dependencies by creating compound names for dependencies.

So we make sure that instead of using React and bundling it into our micro-frontend bundle, we create some sort of a reference to a window object of some sort. In this case, React 16. Second, we want to use this closed list of dependencies that we devised. It includes stuff like React, React DOM, MobX, MobX React, and so on and so forth. We want to use this list and the project's packet JSON to create a bundle per dependency. So what we do during the compilation phase is we go over all of this list. We check which version this micro-frontend has installed and then we create a dedicated bundle for this dependency. In this pseudo code, we created a bundle for React 18, which includes all of its source code. And it also includes a tiny snippet at the end that exposes the source code on the window under a name that includes the dependency's name and its major version.

Once we have that, the final thing is to create a metadata file for the micro-frontend that specifies which shared dependencies and which version this micro-frontend uses. In this example, we specify that we're using React, for example, with the minimal version of 18.1.0, which has the global name of React 18 and is uploaded to this specific URL in our CDN, cdn.com, and so on and so forth. It also includes React DOM and all of the other dependencies that we have on our list. Now that we have all of this, we excluded stuff from our bundle, created bundles, uploaded them to our CDN, and have the metadata file, we're ready to load this micro-frontend in production.

When we go to load this micro-frontend in production, we use the metadata file to ensure that everything is loaded for this micro-frontend to work. Before loading the micro-frontend's code, we go over the shared dependencies it needs, ensuring everything is loaded on the page. If not loaded, we load it. If loaded and compliant, we proceed without action. A unique case is if a loaded dependency isn't compatible with this micro-frontend; in this scenario, a hot swap occurs. Handling sub-dependencies poses challenges as some dependencies rely on components in our list. To address this, we create compound names for dependencies, including all sub-dependencies. This method ensures compatibility within varying combinations of dependencies.

5. Revising Micro-Frontend Hot-Swapping

Short description:

Loading new React versions, addressing sub-dependency issues with compound names, and revising the solution to include global dependencies.

So what we do is we load React 18 a second time, and this time React 18.3.0, and basically we do a hot swap, which is the namesake of this solution, which means we load React 18 again, and we basically override the window.react18 object, and we're now using React 18.3.0, which should be compatible, if Sanver allows, with the previous version, which is 18.1.0. Now, the first problem that we ran into is a problem of sub-dependencies. So some dependencies in our list depend on other dependencies in our list, so for example, MobX React depends on both MobX and React, which both exist in our list, which creates a lot of varying combinations. So let's take two examples, for instance, let's say that one of our micro-frontends has MobX React 8 and uses React 16, and another one has MobX React 8, again the same version of MobX React, and uses React 18. In this case, both micro-frontends are going to use the same global version of MobX React, which is MobX React 8, but the problem is that MobX React under the hood needs a very specific version of React, needs React 18, in order to work properly in runtime and not get a lot of React errors and red screens of death. So what do we do about this? Our fix was very, very simple. We want to make sure that when we create our dependencies, we make sure that we create them with compound names, and that means that every dependency also includes in its global name all of its sub-dependencies. So let's say that we have MobX React, which depends on React. When we add the combination of MobX React 8 and React 16, we're going to do React 16 underscore MobX React 8, and when we have MobX React 8 and React 18, we're going to do React 18 underscore, and so on and so forth. This is a simplified example. We might end up with a situation where we have like three sub-dependencies of differing levels and we might end up with a very long compound name, but that basically ensures that the micro-frontends and their sub-dependencies get the correct reference in production. That also forces us to update the metadata file to reflect this change and include sub-dependencies.

So what do we do about this? Our fix was very, very simple. We want to make sure that when we create our dependencies, we make sure that we create them with compound names, and that means that every dependency also includes in its global name all of its sub-dependencies. So let's say that we have MobX React, which depends on React. When we add the combination of MobX React 8 and React 16, we're going to do React 16 underscore MobX React 8, and when we have MobX React 8 and React 18, we're going to do React 18 underscore, and so on and so forth. This is a simplified example. We might end up with a situation where we have like three sub-dependencies of differing levels and we might end up with a very long compound name, but that basically ensures that the micro-frontends and their sub-dependencies get the correct reference in production. That also forces us to update the metadata file to reflect this change and include sub-dependencies. So this kind of changes our solution a little bit. So instead of building with a simplified name, we use the same list with the same versions we get from Package.json to create the bundle with compound names per dependency. This is the only difference. All of the other steps in the build process and in the runtime process remain the same, and that was it for this issue, but we ran into a different issue.

What we thought would happen when we hot-swap dependencies in production is as follows. Let's say that we load our first micro-frontend that uses React 18.1.0 and it references window.react18, and now let's say that we load a second micro-frontend that uses React 18.3.0. As we mentioned before, React 18.1.0 is not compliant with 18.3.0, which is what our second micro-frontend needs. So we load React again and we replace the window.react18 pointer to point to our new React version, so we loaded React twice. What we expected would happen is that both of our micro-frontends are going to reference window.react18 in the runtime code, and everything would be great, but what actually ended up happening is that our first micro-frontend kept the memory reference to the first instance of React that we loaded on the page. So instead of using the same new instance as our second micro-frontend, it kept using the old instance, which meant that it wasn't garbage collected and we actually loaded React twice to the page, which it kind of misses the point of what we're trying to do here. So this led us to revise our whole solution and basically create a v2 version of our solution, which we called global dependencies. So global dependencies is very similar to the previous solution that we had in both the build process and the runtime process. The only difference being is that during the build process of our micro-frontends, we also update some sort of global metadata file that we have, which we kept on the server. And basically, this global metadata file held references to which micro-frontend in our system had the highest version per major of our dependencies. So in this simplified example, we can see that, for example, if you need to use React18 in your micro-frontend, and it doesn't matter which micro-frontend you are, we know that micro-frontend1 has the highest version of React18 possible on the system. So this might be a situation in which, for example, we have micro-frontend1 is using React18.7.0 and micro-frontend3, for example, uses React18.3.0. When micro-frontend3 is loaded on the page, it's going to use the React18 version that is provided by micro-frontend1 and exists in this URL. This way, we have a list with all of the, including the compound names and all of our dependencies per major version across our system.

6. Resolving Indirect Dependency Issues

Short description:

Handling indirect dependencies with require resolve, managing multiple majors in bundles, and addressing nested dependencies.

And that is because it is not being used directly by the micro-frontends themselves. It is being used by a different package, React Popper in this instance. So that meant that our process hasn't registered that it's being used. And we actually, because it's not installed in the micro-frontends package JSON, it basically didn't create a dedicated bundle for popperjs, and it didn't include it in the metadata of that micro-frontend. So that meant that we're not getting as much as we can out of our solution, and we're not sharing as much as we thought we were. So the fix here was pretty simple. So instead of using the project's package JSON to get the shared dependencies version and determine how to create it, we try to resolve it using require resolve, which is a very simple and neat node function that gives you a path of your dependency that you specify. This leads us to solution V version 2.1, which is similar to V2. The only difference being is that we still use the list, the closed list that we have, but instead of using the project's package JSON to get the shared dependencies versions and create our bundles for them, we now use require resolve, which actually gets us the installed version and everything is great. And at this point, we've been running with this solution for a very long time, until we encountered our fourth problem.

Our fourth problem was that we have multiple majors in the same bundle. So let me paint a picture for you. Let's say that our micro frontend is using MobX, is installing MobX6, directly, it installs it in its package JSON and it uses it to build its data store. And let's say that one of our transitive dependencies, in this case, we installed package A, which uses package B, which in turn uses MobX5, for example. What ended up happening is that both of these would use MobX6, because our solution, basically, if you remember, we're going over the closed list, we're doing require resolve, and we're going to get MobX6. So package B would have a reference to use MobX6 in production, which left us with broken references in production, which was obviously pretty bad. So our fix for that was pretty simple. We wanted to create, actually not that simple, we want to create references to majors per import instead of using the project's package JSON. So how that looks like in our build process, this affected our build process very, very much.

Instead of going over our closed list, our hard-coded list, and creating bundles per install dependency, what we now do is when we build our main bundle, we basically, we remember which dependencies, which one of the dependencies in the list we used, and where it was used from. So whenever we encounter a usage of one of our shared dependencies, we basically try to resolve it from that specific path. So let's say that we encountered the usage in our main bundle, we do require.resolve from that specific path. And let's say that we encounter it again in some sort of transitive dependency, we do require.resolve from that path. So that might leave us with two usages of the same shared dependency, but they're being used from different paths in our node modules, which might mean that they're being used with different major versions. Now that we have all of these references that we created during the compilation process of our main bundle, we can now use that same memory to create a bundle per dependency with compound name per major. So again, what that would look like, it would affect our micro-fondant's metadata file, which now might include multiple majors per package, per shared dependency, but now we have working references in production, which is significantly more important. Our fifth and final problem that we encountered is nested dependencies. So the problem that we have is that we might have some dependencies in our list that aren't being used directly by our micro-fondants. Let's say for example we have popperjs slash core, which is being used by react-popper. So what ended up happening is that when we went over the main bundle of our micro-fondants, we didn't encounter any usages of popperjs slash core.

7. Optimizing Dependency Bundle Building

Short description:

Applying memory of dependencies recursively, creating compound-named bundles for each major version, ensuring readiness for production.

We encountered them only when we built the bundle for react-popper, which is also in our list. So that meant that if we only look at the main bundle of our micro-fondants and only remember which dependencies we used when we built that bundle, we end up with missing references. So in this instance, popperjs slash core. So the fix for that was to apply that same dependencies building logic, that same memory of actually remembering what we used recursively for each one of our dependencies bundles, as well as our main bundle.

So what that looks like in terms of our process, instead of doing this whole remembering thing only for our main bundle, we applied the following logic. While we still have some dependencies to build, we want to use that memory to create a bundle per dependency with a compound name per major, and when we build that bundle, we remember what we used. So this process basically ensures that we keep building new dependency bundles. Obviously, for ones that we didn't build yet. Now we have all of our bundles ready to use in production. All of the other steps in the build process and the runtime process remain pretty much the same.

That's basically it. That's the solution that we ended up with today, at Monday. I think that one of the things that we feared the most is that we're gonna get breakage in production. Of course, this entire thing seems very delicate and prone to breaking. So I have to say that during the three years that we've been running this solution, we haven't run into any issues in terms of Sanver or packages being non-compliant or breaking or something like that. It's important to note that we implemented a lot of kill switches and safety mechanisms around this solution. It was also rolled out as an experimental solution to all of our micro front ends. We rolled it out slowly and gradually over the course of like six months or so. And it's now being the default in all of our micro front ends. And it's working great.

Check out more articles and videos

We constantly think of articles and videos that might spark Git people interest / skill us up or help building a stellar career

Scaling Up with Remix and Micro Frontends
Remix Conf Europe 2022Remix Conf Europe 2022
23 min
Scaling Up with Remix and Micro Frontends
Top Content
This talk discusses the usage of Microfrontends in Remix and introduces the Tiny Frontend library. Kazoo, a used car buying platform, follows a domain-driven design approach and encountered issues with granular slicing. Tiny Frontend aims to solve the slicing problem and promotes type safety and compatibility of shared dependencies. The speaker demonstrates how Tiny Frontend works with server-side rendering and how Remix can consume and update components without redeploying the app. The talk also explores the usage of micro frontends and the future support for Webpack Module Federation in Remix.
Understanding React’s Fiber Architecture
React Advanced 2022React Advanced 2022
29 min
Understanding React’s Fiber Architecture
Top Content
This Talk explores React's internal jargon, specifically fiber, which is an internal unit of work for rendering and committing. Fibers facilitate efficient updates to elements and play a crucial role in the reconciliation process. The work loop, complete work, and commit phase are essential steps in the rendering process. Understanding React's internals can help with optimizing code and pull request reviews. React 18 introduces the work loop sync and async functions for concurrent features and prioritization. Fiber brings benefits like async rendering and the ability to discard work-in-progress trees, improving user experience.
Full Stack Components
Remix Conf Europe 2022Remix Conf Europe 2022
37 min
Full Stack Components
Top Content
RemixConf EU discussed full stack components and their benefits, such as marrying the backend and UI in the same file. The talk demonstrated the implementation of a combo box with search functionality using Remix and the Downshift library. It also highlighted the ease of creating resource routes in Remix and the importance of code organization and maintainability in full stack components. The speaker expressed gratitude towards the audience and discussed the future of Remix, including its acquisition by Shopify and the potential for collaboration with Hydrogen.
Thinking Like an Architect
Node Congress 2025Node Congress 2025
31 min
Thinking Like an Architect
Top Content
In modern software development, architecture is more than just selecting the right tech stack; it involves decision-making, trade-offs, and considering the context of the business and organization. Understanding the problem space and focusing on users' needs are essential. Architectural flexibility is key, adapting the level of granularity and choosing between different approaches. Holistic thinking, long-term vision, and domain understanding are crucial for making better decisions. Effective communication, inclusion, and documentation are core skills for architects. Democratizing communication, prioritizing value, and embracing adaptive architectures are key to success.
Monolith to Micro-Frontends
React Advanced 2022React Advanced 2022
22 min
Monolith to Micro-Frontends
Top Content
Microfrontends are considered as a solution to the problems of exponential growth, code duplication, and unclear ownership in older applications. Transitioning from a monolith to microfrontends involves decoupling the system and exploring options like a modular monolith. Microfrontends enable independent deployments and runtime composition, but there is a discussion about the alternative of keeping an integrated application composed at runtime. Choosing a composition model and a router are crucial decisions in the technical plan. The Strangler pattern and the reverse Strangler pattern are used to gradually replace parts of the monolith with the new application.
The Eternal Sunshine of the Zero Build Pipeline
React Finland 2021React Finland 2021
36 min
The Eternal Sunshine of the Zero Build Pipeline
For many years, we have migrated all our devtools to Node.js for the sake of simplicity: a common language (JS/TS), a large ecosystem (NPM), and a powerful engine. In the meantime, we moved a lot of computation tasks to the client-side thanks to PWA and JavaScript Hegemony.
So we made Webapps for years, developing with awesome reactive frameworks and bundling a lot of dependencies. We progressively moved from our simplicity to complex apps toolchains. We've become the new Java-like ecosystem. It sucks.
It's 2021, we've got a lot of new technologies to sustain our Users eXperience. It's time to have a break and rethink our tools rather than going faster and faster in the same direction. It's time to redesign the Developer eXperience. It's time for a bundle-free dev environment. It's time to embrace a new frontend building philosophy, still with our lovely JavaScript.
Introducing Snowpack, Vite, Astro, and other Bare Modules tools concepts!

Workshops on related topic

AI on Demand: Serverless AI
DevOps.js Conf 2024DevOps.js Conf 2024
163 min
AI on Demand: Serverless AI
Top Content
Featured WorkshopFree
Nathan Disidore
Nathan Disidore
In this workshop, we discuss the merits of serverless architecture and how it can be applied to the AI space. We'll explore options around building serverless RAG applications for a more lambda-esque approach to AI. Next, we'll get hands on and build a sample CRUD app that allows you to store information and query it using an LLM with Workers AI, Vectorize, D1, and Cloudflare Workers.
Micro Frontends with Module Federation and React
JSNation Live 2021JSNation Live 2021
113 min
Micro Frontends with Module Federation and React
Top Content
Workshop
Alex Lobera
Alex Lobera
Did you ever work in a monolithic Next.js app? I did and scaling a large React app so that many teams can work simultaneously is not easy. With micro frontends you can break up a frontend monolith into smaller pieces so that each team can build and deploy independently. In this workshop you'll learn how to build large React apps that scale using micro frontends.
High-performance Next.js
React Summit 2022React Summit 2022
50 min
High-performance Next.js
Workshop
Michele Riva
Michele Riva
Next.js is a compelling framework that makes many tasks effortless by providing many out-of-the-box solutions. But as soon as our app needs to scale, it is essential to maintain high performance without compromising maintenance and server costs. In this workshop, we will see how to analyze Next.js performances, resources usage, how to scale it, and how to make the right decisions while writing the application architecture.
Micro-Frontends with Module Federation and Angular
JSNation Live 2021JSNation Live 2021
113 min
Micro-Frontends with Module Federation and Angular
Workshop
Manfred Steyer
Manfred Steyer
Ever more companies are choosing Micro-Frontends. However, they are anything but easy to implement. Fortunately, Module Federation introduced with webpack 5 has initiated a crucial change of direction.
In this interactive workshop, you will learn from Manfred Steyer -- Angular GDE and Trusted Collaborator in the Angular team -- how to plan and implement Micro-Frontend architectures with Angular and the brand new webpack Module Federation. We talk about sharing libraries and advanced concepts like dealing with version mismatches, dynamic Module Federation, and integration into monorepos.
After the individual exercises, you will have a case study you can use as a template for your projects. This workshop helps you evaluate the individual options for your projects.
Prerequisites:You should have some experience with Angular.