Microfrontends as Monolith? Shared component library or styleguide? This technique allows to consume modules from separate builds, which can be developed and deployed independently. An introduction, and further ideas.
Module Federation in Webpack 5
AI Generated Video Summary
This Talk discusses module federation in Webpack 5 as a scalable solution for shared dependencies in large applications. Module federation allows separate builds for different parts of an application, reducing build time and deploy delay. It features exposed and shared modules, asynchronous loading, and container creation. Module federation supports container orchestration techniques and aims to integrate with ECMAScript modules. However, optimization and sharing in module federation may impact code size, and careful evaluation is necessary. Updating containers can be managed through active testing for stability.
1. Introduction to Module Filleration in Webpack 5
I'm going to talk about module filleration in Webpack five. The motivation is you have a large to mid scale application and you work with multiple teams on multiple application paths. Let's look at the existing options with Webpack. There's this option about just doing a single build and you can build all your applications in all these parts together.
So let's get started. I worked for the Webpack core team and I'm going to talk about module filleration in Webpack five. So my talk is about a module fillervation in Webpack five. And I want to tell you about the motivation for this feature, how it works and what, how to use it.
So the motivation is you have a large to mid scale application and you work with multiple teams on multiple application paths. So you have separated your application or applications into multiple parts like micro-font ends, but also logical parts. And these parts would be developed independently by different teams. And another requirement is that you have a multiple part sharing common libraries or sharing on other paths. Here's an example. And you could have been header component and site component as micro-font end on pages. Or you could also have a style guide components library, which is shared by all the applications. But also different things than font ends like data fetching logic, business logic, or as a logical components.
So let's look at the existing options with Webpack. You could just go with native ECMAScript modules. This is maybe you don't have any build process for linking the parts together. You could just consume natively all your modules in the applications, and it would use native browser import statements to link them together. But there are some challenges with this approach. You basically opt out of all optimizations Webpack would do for you like unused explorers, concatenation of modules, other optimizations. And there are also some challenges about web performance. Like you would have each module separately. So it's causing a high count of requests at one time. This has each request has an overhead and you get less effective compression with more requests and smaller files. Of course, larger files usually compress better than in smaller files. And you have all of this drawback about only being able to use ECMAScript modules and you can't use CommonJS modules. You can't use CSS modules, ESM or other a lot of processed things like other languages also in Webpack.
There's this option about just doing a single build and you can build all your applications in all these parts together. And this way every module of every other part or application is accessible during the build process. So you can just use them via port statements. But there's also a few changes with that approach. Each update requires a full build of all applications and all parts.
2. Challenges and Module Federation
So it has a high build time and this means high deploy delay from update to deploy the new version of the application. But there's a plugin in Webpack which allows you to separate a part of your build process into a separate build, which can be built independently. An alternative to this is externals and built-in libraries. So, to summarize this, native ECMAScript modules are problematic because of problematic web performance. A single build process is problematic about build performance. And the DLL and externals approach would work, but they require a lot of manual work to extract shared libraries or so on. So, in the end, we need a scalable solution, or at least a trade-off which has good build performance, good web performance, but also a good solution for shared dependencies. That's why we enter Module Federation.
So it has a high build time and this means high deploy delay from update to deploy the new version of the application. And you also have this problem it's that you can't separately build each application. So your applications don't stay separate from each other because if you want to share common parts or common modules or common libraries at one time you have to build them together. So it's like a challenge you have to come up with.
But there's a plugin in Webpack which allows you to separate a part of your build process into a separate build, which can be built independently. In this scenario you would build each part as DLL with a so-called DLL plugin. And these DLLs can be consumed at one time by the other by the consumer consuming build. But you also have a compile time dependency of the DLL generated manifest at compile time. So that's also one challenge and you have to rebuild your application when a part has changed or consumers have to be rebuilt. And it's an additional deploy delay. It's not so high compared to the single build approach but it's still an additional deploy delay you don't want to have. And there's also a big challenge about sharing libraries or sharing common modules. And basically if multiple parts share a library you have to pull out or extract this shared library into a separate DLL and separate process manually and then consume the DLL generated by the separate process by all part sharing this library. So it's a lot of manual work involved to be able to share libraries between parts.
An alternative to this is externals and built-in libraries. So each part, in this scenario, each part would be built as library and then consumed by the consuming parts of applications as externals. This eliminates this compile-time dependency between parts and consuming parts and other modules could be just consumed from the library at runtime. But still, the challenge about sharing libraries stays true for this scenario. Each shared library has to be extracted into separate process, separate library, and then be external in each of these consuming parts or consuming applications.
So, to summarize this, native ECMAScript modules are problematic because of problematic web performance. A single build process is problematic about build performance. And the DLL and externals approach would work, but they require a lot of manual work to extract shared libraries or so on. So, in the end, we need a scalable solution, or at least a trade-off which has good build performance, good web performance, but also a good solution for shared dependencies. That's why we enter Module Federation. In Module Federation, you would build each part separately. And here we would build a so-called container, and each part would be published or deployed as container. And any application or other containers could consume modules from this container. In this relationship, the consumer is the host, and the container would be the remote. And if the host consumes exposed modules from the container, then they would be called remote modules. And so we got back again to the separate, each part is built separately, independently and deployed independently, so we have this good build performance.
3. Module Federation Features and Shared Modules
You only pay for the build time of your part you're developing. Module Federation comes with two features: exposing modules and shared modules. Exposed modules are asynchronously loaded, minimizing requests and loading only what you're using. Shared modules can be de-duplicated within the share scope by version taking. An example is given with a normal application and two teams working on different components.
You only pay for the build time of your part you're developing, or you update it. And the whole process looks like this.
Module Federation comes with two features or aspects. And the first aspect is exposing modules. A container can expose modules, and they can be consumed by the host via remote modules. An exposed module would be asynchronously exposed, so this means if you request a module from the container, this would be an asynchronous process, and the container would then only load the code related to your exposed modules. This means you would only have to pay the cost of the container and the cost for the modules you're really using from the container. So only the code is downloaded from the modules you are using. But the containers then can still do optimizations like bundling dependencies of exposed modules together or extracting shared parts of exposed modules and automatically, or the optimization they can do for automatic loading is possible for exposed modules. So this brings back the good web performance by minimizing requests and also only loading what you're really using.
So the second aspect is the shared modules aspect. In this aspect, each part or each participant of the application landscape can provide modules, provide shared modules into the share scope. These modules are provided with version information attached. So I like the container could expose React in a version 60.3 and that would be put into the share scope. And on the other side and containers or consumers or every participant of the landscape can consume modules from the share scope with a version check. So the container may be asked for React in a version 60.0 or higher and then it would get the highest version available in the share scope. And this way, shared modules can be de-duplicated within the share scope by version taking and you don't have to download React twice if you're having a shared module. For shared modules, the same asynchronous loading is there, like with exposed models. So you're only, you would put every shared module in a separate file or separately download it and you only have to download shared modules you're really using. So you can provide older React versions, but if you're only using the newer version, it would not download any old version.
So here's an example of this in work. So we start with just this normal application which has an homepage and on the homepage there's a login link and the login link opens the login module and shows like a button which like called login or so. And on the homepage there's a dropdown and which shows something. Everything uses React in this scenario, but the login module code is loaded asynchronously when clicking on the login button. So using on-demand loading for this scenario to move this into a separate chunk or move it as separately downloaded. In this scenario we have two teams working on this. So team A is working on homepage and login model, so this is components. And team B is working on the component library where it has a dropdown component and also the button component. And this is how it works with a single build. Everything is super optimized, but let's apply module federation on this concept.
4. Module Federation and Container Creation
Team B flags their own components and dependencies as exposed modules in the graph. Webpack builds a container with separate files for each exposed module and shared module. When requesting a component, the corresponding chunk and its shared modules are loaded in parallel. Team A uses the container as remote modules, with the challenge of loading modules asynchronously. The module federation plug-in in Webpack 5 allows for creating a container by exposing modules using the exposes property.
So from the view of team B, team B only cares about their own components, like the button components and dropdown component, but also their dependencies, like an arrow icon is used for the dropdown and also the React S library. And to use module federation team B would flag these modules in the graph, like button is exposed, dropdowns exposed. So exposed means they are available by consumption from the container interface. And also flag React as shared library. So they may be shared with other teams at one time. So now Webpack would create a built, would build a container for all of this.
So in this container, there would be a container entry module generated by Webpack, which contain references to all the exposed modules, like button or a dropdown. And Webpack would also put every exposed module into a separate file. So like here's button in a separate file, but also dropped on a separate file, but still be able to bundle dependencies into the togetherness of the exposing module. So chunking optimization still applies. It would also put every shared module into a separate file to be, because it may be loaded or maybe not loaded, depending on if already a vector version is available at one time. So if you take some examples, like if you request the button component from the container, and the runtime would load button chunk. So the file would contain the button, but also the shared modules of this and required by this chunk, they would load them in parallel. And in the scenarios that if there's already a React version, same or higher at one time available, then the request and dropdown would only require their own dropdown chunk and the React chunk would be loaded from somewhere else or not loaded at all if it's already been loaded before.
Now let's team A use this container generated by team B. This is how the module graph looks for team A. Team A only cares about their own components and each component from team B would be edit as remote modules. So I only saying there's a container and there's somebody somewhere and a consuming dropdown from the container. So from Webpack point of view, there's a remote module, like the dropdown module and every remote module points to the container as external at one time. But there's a challenge here because loading modules from the container asynchronous, we have to be a bit creative to solve this problem because if you, a login module just imports the button component and importing is usually a synchronous operation. So a Webpack would automatically hoist every asynchronous operation required for loading this remote modules up to the next async boundary. An async boundary is like an asynchronous import statement or something like that. So in this case, if you click on the login link, which usually loads the code for the login module, it will in parallel load the code for the button component from the container. So you click the login link and login module code and button component code would load in parallel once from the local build, once from the container build. Yeah. And the same happens for shared modules but it's not in detail explained here. Cool. So now how can I use it? To use it, there's a module federation plug-in available in Webpack 5 and with different properties you have access to creating container, consuming other containers, but also sharing modules at one time. Let's look at creating a container. So to create a container, you have to expose modules from the container, which is done by using the exposes property.
5. Module Federation Features
The exposes property gives each module a public and local name. The remotes key is used to consume other containers. To share modules, use the shared property. Advanced configuration is available for module sharing and container building.
So the exposes property has some properties like give each module a public name, like tracking system or data, and a local name, which is like where you module is on the disk currently from this bit, and to each module supported, can be just a normal ECMAScript module, could be common JS module, could be CSS, could be anything processed by loaders, whatever.
And to consume other containers and you have to use the remotes key, or remote properties in the modules federation plugin, and here you give each container a name like analytics, and all the point to a container location which is then loaded at one time, here it would load the analytics JS script at one time.
To use these remote modules from containers, you would just have an import statement with analytics, is previous with analytics, and then the public name of the module in the container, the exposed module in the container, here, tracking system. Yeah.
And to share modules, you have the shared property. And in this shared property, you just list all the modules you want to have, want to be able to share between other containers or other applications. Like an example here, react-virtualize-shared, but also react-virtualize-the-styles of react-virtualize. And there's also advanced configuration available. Here, an example is a react, must only be a log instantiated once at an HTML page. So you can use a single advanced configuration to make sure react is only your loaded once. For each shared module, Webex will figure out which version is provided by looking up the version information from the package.json, but also would look up the required version for each dependency by looking in your package.json dependencies list or a depth dependencies list. And yeah, but you could also use more advanced configuration to override this or pass other things or define fallback modules, define different keys as also advance configuration for container building and also for consuming containers available. If you're interested, pause video and look in the details.
6. Module Federation and Container Orchestration
At large scale, you have multiple applications using component libraries, separate containers for pages, and shared business logic. There are two techniques: evergreen, where applications always use the latest container version, and managed, where applications log the container version and actively upgrade. Module Federation allows different builds to act as a monolithic landscape, supporting various Webpack modules and enabling semantic versioning. It only loads modules that are used, but all exports are available. Federated modules are asynchronous and require asynchronous loading boundaries. More advanced features may come in the future.
At large scale, this could look like this. You have multiple applications which are using component libraries, we are using pages as separate containers and also business logic is shared or translation stuff as well. So there's all of this funny aspect about orchestration.
So now we have built all this container and separately but you have to put them together at one time. And there are basically two techniques I came up with. The first technique is evergreen. And this means each application always uses the latest published version of a container. In this scenario, if you publish a change to your design systems components, then every application would instantly get the latest version of this at runtime and there's no additional step between that. It's like having a package station with a log file and running npm install every time of the application in the browser. It's a bit risky, but it could work on a smaller scale because your company is in control of all your containers. So if you don't make a mess, this could work for you.
But there's also another approach, I called it managed. And in this scenario application would log the version of the container they are using. And it's like having a package that has a log file. And just like with a log file, there's an active step to upgrade or update the version of your containers which your application is using. So you can validate if this update to your containers works for your application. This is where you can test on application level if containers are still compatible with your applications. And yes, could be multiple stages like staging production environment or so on. Here are some ideas how to solve this, but I don't want to go into detail. You can pause the video if you're interested.
So to make a summary, Module Federation allows you to make different builds act as monolithic, monolithic up a landscape at one time. Any Webpack supported module is supported like Echoscape, CommonJS, but also ESSERTS, a lot of process stuff or CSS. Sharing modules is available to do semantic versioning at one time. It's available for all the Pack Times like Webnode, Web Worker, et cetera. And it will only load downloads and modules you're really using, but it will always ever use all exports of them, so you have to be aware that there's a trade-off about optimization here. And federated modules are asynchronous, so you need to have asynchronous loading boundaries somewhere in the graph before you're able to use federated modules. So if you just experiment, so maybe break and traces come in future, maybe there are more advanced features in future, yeah, I already have some ideas which could come, but don't want to go into detail now. So here are some of the sources you can, if you're interested, look up into the documentation, other talks, and more examples are available online. So I have to say, thanks. But let's go deeper into the topic of the federation.
Integration of ES Modules and Module Federation
There are plans to make Webpack ECMAScript modules supported, but there are technical challenges. The ECMAScript module format as output format for Webpack is not enabled yet, but it is being worked on for Webpack 5. The goal is to provide compatibility with ECMAScript modules, but there are complexities when combining with CSS or other features. Webpack aims to balance forward progress with backward compatibility and stability. As for benchmarks on bundle size and load time with module federation, there are no specific benchmarks, but there are examples available to explore and the goal is to maintain the one-roundtrip limitation for loading on demand.
Tobias, can you join me on stage? Sure. Thanks for inviting me. Yeah, good to have you. I have to say you're a brave man tackling these issues. So I don't wear a hat right now, but hats off to you, and awesome that you made it understandable for a person like me who's not into these build systems.
We're gonna dive into our audience question. And the first one I have is from Albert G. Is there any chance Webpack will make it possible to integrate ES modules as DLLs or convert a Webpack proprietary module system to be compatible with ES modules? So I think with DLLs he means containers in terms of modification also. So yes, there are plans to make Webpack ECMAScript modules supported. So currently we can use ECMAScript modules as external. So if you have a container in the ECMAScript format you would be able to launch this container but you can't produce a container in ECMAScript format. You can't. Basically the ECMAScript module format as output format for Webpack is not enabled yet. We don't have it yet, but we are working on this for Webpack 5 and I think it will be available for the final release but there are some technical challenges and not about basic JS. That would be straightforward. But if you have combined it with CSS or this complex stuff like if you have interrupt logic in the bottle like common.js and questions about strict mode like ECMAScript modules are always in strict mode but common.js usually can only opt into strict mode. So it would have some behavior changes to modules. So these are the questions we are considering. So it's kind of very difficult to make some future like this comes with other builders like straightforward but with Webpack we have a lot of features we have to support if we want to provide something like that. So it's not that easy for us to add this but we are planning to add this and we probably add some limitation to that and so you can't use it with some complex structures or we have some workarounds to make it work. But if you can tie it into the panel discussion we have before, that you have to, yeah, you don't wanna be stuck in the past you wanna continue going forward but yeah, you can't leave everyone behind. So that of course slows you guys down you could be down as well. I think a lot of value in Webpack is the stability and the backward compatibility for existing code. So we have a lot of large user base and we don't want to leave them back with newer versions. We want to keep the existing features supported but again, yeah. Thank you, thank you for that.
We're gonna go to the next question from Tudor. Are there any benchmarks on bundle size or load time results when using module federation? So I don't have benchmarks but SecJackson has a big repo with a lot of examples and you can play around with a lot of sample cases with module federation. But to summarize it, we want to keep, and so basically, Bepack is based on if you load something or not something on demand, it should only take one roundtrip to the server and download everything and maybe make parallel requests to not all the stuff. And you want to keep this, you keep this limitation, keep this goal for module federation, so it will still take one roundtrip to the server.
Optimization and Sharing in Module Federation
With module federation, optimization ability may be lost, leading to increased code size. Sharing modules requires providing all exports, which can result in larger module sizes. However, sharing modules can also decrease final size by enabling sharing between different parts of the application. The impact depends on the specific user landscape, so it is important to measure and evaluate the results.
But with module federation, you could lose some optimization ability. So it could increase the code size if you share modules with others, we have to provide all exports of the module. So we don't know which exports are used in the whole application landscape. So we basically prepare our modules to be used by anyone. So we can do less optimization at this boundary where shared modules or exposed modules come together with other one-time-loaded containers applications, which we don't know at compile time. So it could increase the final size, but it also could decrease it by sharing the ability to share modules between this kind of micro-funded parts of the application. So the short answer, it depends. It depends. Yeah, that's always hard, of course, it depends on the landscape of the user. All right. Always measure. Yeah. It's always the user.
Affected Apps and Updating Containers
There are two different approaches to figuring out the affected apps based on updating a container. The evergreen approach does not provide information about container changes, while the managed approach involves actively testing the application with updated containers before deploying them. This approach ensures stability when containers are updated.
The next question is from Amaya. Is there any way to figure out the affected apps based on updating a container? Yeah, so there's two different approaches in my talk. So evergreen, so you don't get to know when a container changes. But the managed approach is about verifying that the application still works with updated containers. So in this approach, you would actively test your application with the updated containers, make sure everything works, and then deploy the new application specification with new latest version of the container. So this is the way you want to do if you have considerations about stability when containers are updated and so on.