nx
nx copied to clipboard
Slow build for monorepo with >200 buildable libraries
Current Behavior
At this moment we have 291 buildable libraries in our repo and one application that uses them all. Before migration from Angular application to NX Angular application with libraries, we had a production build for 6-8 minutes. At this moment, when we trying to cold build an application in production, it takes 15-25 minutes with a parallel set on 20. Of course, a warm build is faster, but in our company, people work on different domains, so on average every day we have a cold build because someone changes sth at the bottom of the tree. So for us it's look like a regression, because instead of 6-8 minutes, now we have 15-25.
Expected Behavior
Are there any solutions, or plans to improve build time for libraries? Or maybe you have any tips on how to organize a repository, to build faster. I think here a lot of companies that using your solution and maybe struggled with the same problems
Steps to Reproduce
Repository with >200 buildable libraries
Failure Logs
Logs from the terminal:
Build at: 2022-09-13T10:49:17.325Z - Hash: 949bfc4090c9cbc7 - Time: 250220ms Done in 958.32s.
Environment
Node : 16.16.0 OS : darwin x64 yarn : 1.22.4
nx : 14.5.8 @nrwl/angular : 14.5.8 @nrwl/cypress : 14.5.8 @nrwl/detox : Not Found @nrwl/devkit : 14.5.8 @nrwl/eslint-plugin-nx : 14.5.8 @nrwl/express : Not Found @nrwl/jest : 14.5.8 @nrwl/js : 14.5.8 @nrwl/linter : 14.5.8 @nrwl/nest : 14.5.8 @nrwl/next : Not Found @nrwl/node : 14.5.8 @nrwl/nx-cloud : 14.4.1 @nrwl/nx-plugin : Not Found @nrwl/react : Not Found @nrwl/react-native : Not Found @nrwl/schematics : Not Found @nrwl/storybook : 14.5.8 @nrwl/web : 14.5.8 @nrwl/workspace : 14.5.8 typescript : 4.7.4
Local workspace plugins:
Community plugins: @ngneat/transloco: 4.1.1 @ngrx/effects: 12.5.0 @ngrx/entity: 12.4.0 @ngrx/store: 12.5.0 @ngrx/store-devtools: 12.5.0 @rx-angular/cdk: 1.0.0-beta.2 @rx-angular/state: 1.6.0 @rx-angular/template: 1.0.0-beta.32 @storybook/angular: 6.5.10 @testing-library/angular: 11.0.4
With Angular, buildable libs has an overhead in the linking stage of the webpack build for the application. This reduces the potential time savings when using buildable libs with Angular apps.
To get better time savings it may be worth looking at Module Federation for purely incremental builds.
We have a video about this: https://www.youtube.com/watch?v=JkcaGzhRjkc
We should highlight this more in our docs.
Hi, thank you for your response @Coly010. Ok, but if I will divide my application into smaller ones, it doesn't solve the problem. For example, one small application will contain 100 libraries. I have the newest Macbook Pro with M1 and for me, building 100 libraries takes ~8 minutes with a parallel set on 20. I think here might be a problem, because instead of build application in 10 minutes, I'm building libraries for 8 minutes + application. I know that cold build might be longer than normal, but as I told you in the thread. I work in a company and every day we have a lot of commits and changes done on different application levels. So every day I will have a cold start
Maybe it needs more investigation?
It has been investigated pretty thoroughly.
We recommend either sticking to using non-buildable libs or using Module Federation.
As mentioned before we have a video that discusses this: https://www.youtube.com/watch?v=JkcaGzhRjkc
You mention you have one application containing 100 libraries, it would be worthwhile looking at how you could split this into smaller chunks, with less libraries required for each chunk.
the more you can divide in this format, the better benefits you will receive from using local cache and distributed caching via Nx Cloud.
With Nx Cloud, even if lots of people are making commits every day, the distributed cache will ensure that you benefit from everyone else's caching of their builds + CI's cache of the build.
Can you correct me, so that if I will switch to non-buildable libs, we won't have a cache?
The build of libs will not be cached no, however, the testing and linting will still be cached. Your app will be, but as you mentioned, it will be invalidated often.
But the cache of buildable libs will not be giving you much benefit due to the overhead of Angular Ivy and webpack linking stage of the application build. There will be some benefit, but it will be minor.
You'll see a much bigger time save from using Module Federation.
If you split your application into multiple domains (vertical slices) then you'll get a better build time, because the cache for each domain can be used as is without the overhead of Angular Ivy and webpack linking stage, as the built artifacts (that are cached) will be fetched at runtime, rather than included at build time.
This does mean you need to deploy slightly differently, but the resulting time save is huge.
Picture your app like this
app
/ | \
featureA featureB featureC
If featureA
featureB
and featureC
are all Module Federated apps, then if you make a change in the featureA domain, it will not invalidate the cache of featureB
or featureC
.
As app
does not depend on them at build time, only run time, then they don't need to be included in your rebuild, unlike buildable libs, which always need to be included.
Theoretically, you could even build and deploy featureA
without having to rebuild and deploy app
at all. So you reduce your build time to only the one domain (vertical slice) of your codebase involved in featureA
. this can drastically reduce build times and increase productivity.
Thank you @Coly010 :) I need to analyze it. I can close it at this moment
Hi @Coly010! I tried module federation in the simple project from your GitHub repository: (https://github.com/nrwl/ng-module-federation) and I have a question about the global state. I tried to create a shared buildable library with one service which is in the forRoot method and imported to the host application. And when I'm trying to serve application with this command:
yarn nx serve host --devRemotes=about
Changes inside my global service were not applied. What is interesting, is that in dev tools from Chrome Browser I see these changes, but for some reason, I'm getting the wrong value :D What do you think about it? Do you know how to solve this problem? I think it's important, because this is the core feature
When working with buildable libs you need to make sure that the buildLibsFromSource value in the development configuration of your build is set to true, and then in your production build configuration have it set to false.
As I know "buildLibsFromSource" by default has "true" and I just checked my repo I have no this parameter inside my project.json. So possible it's not a reason?
@B1Z1 can you share the repo with me?
Yes, here's the link: https://github.com/B1Z1/ng-module-federation
Execute: yarn nx serve host --devRemotes=about
As I understand, that means, that we build our whole host application with all dependencies and serve "about" app. So you can find in libs "test" library. I imported TestModule to host -> AppModule. When you first open application and go to about page, you will see console log in dev tools "About: Initial value".
After you can change in TestService text variable to another text and try to reload the page.
It must look like on screen

Hi @Coly010, had you tried this?
@Coly010 any update? For us, it might be a blocker, cause we have a lot of global services if I want to divide application
I found the issue. It's specifically an issue with Angular + Module Federation.
If you look at the network tab, you can see that there is a bundle being downloaded that includes the updated value for TestService. The problem is the static remotes (shop, cart) are not being rebuilt, and they contain a reference already to the same Service in their bundle. Because TestService is a Singleton in Angular, even after you change the value of TestService, the About remote will have the new value in its common.js bundle, but because the service is already registered as a Singleton, and the two static remotes already have the old value in their bundles, the Singleton instance is not replaced (as expected, it's a Singleton).
You can get around it by having all bundles be served nx serve host --devRemotes=cart,shop,about
The alternative is to stop the process and run nx serve host --devRemotes=about
again. The static bundles will be rebuilt.
This is important, you're changing a file that affects all remotes, so they NEED to be rebuilt. Despite only working on one area, if you touch a global service then all remotes need to be rebuilt.
I think about nx serve host --devRemotes=cart,shop,about
. Will it have differences between normal nx serve app
and nx serve host --devRemotes=cart,shop,about
? for me, it looks like the same watch as before, but with several servers.
From my side, I'll try to introduce MFE in our application and will analyze if is it a real boost for everyone. Thank you for answer :)
No, there wont be much difference. The real value at dev time comes from being able to use static serve, which would be when you don't specify all --devRemotes
. But that said, if you're touching global services that are available in all remotes, then you need to rebuild them all anyway.
So if you are only working in one remote, nx serve host --devRemotes=myRemote
is the best way to go. If you touch a global service, re-run nx serve host --devRemotes=myRemote
. It will automatically rebuild the other remotes, and still provide live updates for every other aspect of your specific remote myRemote
.
Also last question and we can close it. As I understand, to start to watch every remote, every developer needs to build every library which depends on devRemote before. So if for example, one module will depend on 100 libraries, the developer will need to build them all. So my question is if libraries will grow and for example, it will increase from 100 to 200, we will need to consider dividing the remote module into several modules or there's another solution?
That's a valid solution yes. It's a good idea to frequently revisit the architecture to see where you can gain performance. If some of those libraries are very rarely touched, they could be turned into buildable libs so that they can be cached, and if unchanged the lib will be taken from the cache rather than rebuilt. That comes with its own trade-offs too however, so it's all about weighing up the options and choosing what makes sense for you and your business.
Okay, thank you so much! I'm closing the ticket right now
This issue has been closed for more than 30 days. If this issue is still occuring, please open a new issue with more recent context.