1

I've created a Bootstrap component library for angular along with a demo application. However when building the application, the main bundle size is massive. More remarkably, adding components to the library, and adding pages (lazy-loaded) to the application which load these modules, recently increased my main bundle size from 650kB to 840kB.

I already checked other questions like this one too, but it doesn't give me an answer.

EDIT 5

I recreated the project in an angular workspace, with dist/lib-name refs in the tsconfig.json. Even here I can come to a very simple conclusion:

  • With no references to a library component in the AppModule, the main bundle is simple and clean, as expected (328 kB):

Main bundle, with no references to library modules

  • From the moment I use a single component in the application, tons of javascript chunks from the library are bundled in the main bundle (928 kB). Even the most simple BsAlertComponent:

Main bundle, after using the Navbar on the AppComponent

EDIT 4

I recreated my project in an angular workspace, with the expected result:

Main bundle for angular workspace

Total main bundle size (js): 509 kB. The main bundle only contains the navbar chunk

Then for the NX workspace with a single library:

Main bundle for NX workspace

Total main bundle size (js): 695 kB. Note all the unnecessary chunks. I'm pretty sure there are no index-imports (import {} from '../navbar' instead of import {} from '../navbar/navbar.module') in my project.

EDIT 3

I seem to have found a catch here. While recreating my workspace step-by-step, here's what I see:

When there's no animations on my component No animations active on any component

This is my main bundle Main bundle without animations

This is the common bundle (lazy-loaded) * Common bundle without animations Contains BsListGroup and BsCard

And the lib/components chunk from the main.xxx.js bundle looks like this: Main components chunk without animations Contains only the navbar

Now let's put an animation on the BsAlertComponent: I put an animation on my component

This is my main bundle Main bundle with animations

My common bundle (containing BsListGroup and BsCard) looks exactly the same as *

However, the components chunk from the main bundle looks like this Main bundle/components chunk with animations And clearly contains the entire BsAlertComponent (which I don't need) and tons of garbage from other components in the project...

PS. Please fix the SO file-uploader...

EDIT 2

I created a minimal angular workspace (even without a library) and I can see all the same behavior as I described in my old question (see below).

The app contains 3 components each with their respective module, 2*2 pages. The BsNavbarModule is loaded on the AppModule because I'm using the BsNavbarComponent in the root.

@NgModule({
  declarations: [
    AppComponent
  ],
  imports: [
    BrowserModule,
    BsNavbarModule,
    AppRoutingModule
  ],
  providers: [],
  bootstrap: [AppComponent]
})
export class AppModule { }

Each page loads the respective component.

Carousel 1 page:

@NgModule({
  declarations: [
    CarouselOneComponent
  ],
  imports: [
    CommonModule,
    BsCarouselModule,
    CarouselOneRoutingModule
  ]
})
export class CarouselOneModule { }

Offcanvas 1 page:

@NgModule({
  declarations: [
    OffcanvasOneComponent
  ],
  imports: [
    CommonModule,
    BsOffcanvasModule,
    OffcanvasOneRoutingModule
  ]
})
export class OffcanvasOneModule { }

So what would you expect to see here?

  • main bundle
    • AppModule -> AppComponent
    • NavbarModule -> BsNavbarComponent
  • carousel-one bundle
    • CarouselOneModule
  • carousel-two bundle
    • CarouselTwoModule
  • bs-carousel bundle -> BsCarouselModule
  • offcanvas-one bundle
    • OffcanvasOneModule
  • offcanvas-two bundle
    • OffcanvasTwoModule
  • bs-offcanvas bundle -> BsOffcanvasModule

What do we get?

  • main bundle -> NavbarModule

Main bundle contains the NavbarModule

  • bundle with the Carousel1Page
  • bundle with the Carousel2Page
  • bundle with the Offcanvas1Page
  • bundle with the Offcanvas2Page
  • One big common bundle with both the BsCarouselModule and BsOffcanvasModule (this is not what I expect it to be)

A common bundle with all components

  • main, polyfills, runtime, styles

Why are all these components bundled together into a single bundle? When the user visits a page, it's not necessary to download the js for all these components... In this example it's only about 2 components, but in my real application there's like 30 components, of which most are bundled together for no reason.

OLD QUESTION

So to rephrase, the pages are lazy-loaded obviously:

import { NgModule } from '@angular/core';
import { RouterModule, Routes } from '@angular/router';

const routes: Routes = [
  ...
  { path: 'range', loadChildren: () => import('./range/range.module').then(m => m.RangeModule) },
  { path: 'select', loadChildren: () => import('./select/select.module').then(m => m.SelectModule) }
];

@NgModule({
  imports: [RouterModule.forChild(routes)],
  exports: [RouterModule]
})
export class BasicRoutingModule { }

And only in these pages I'm loading the modules from my library. I ran the webpack bundle analyzer on the main bundle, and noticed that all modules from my library are bundled in my main bundle, just because I'm loading the BsNavbarModule in the AppModule.

npm run build:wba
npm run analyze:wba

Main bundle

Visualization of the production bundle

The Stuff in red is all part of the main bundle, even though I never import the component's module in the AppModule, only in lazy-loaded modules. The BsNavbarComponent is used on the AppComponent, so the BsNavbarModule on the other hand needs to be in the main bundle. I'm also under the impression that because of all this, the @angular/core and @angular/common bundles are a lot bigger than they actually need to be.

Another catch is that I'm using ngx-highlighjs in some lazy-loaded page, and this library requires you to specify the HIGHLIGHT_OPTIONS at root level. Because of this, you can also see the entire ngx-highlightjs library packaged in the main bundle, even though I only load the module on yet another lazy-loaded page...

ngx-highlightjs is in the main bundle

The module of my library is specified as "esnext".

I tried adding "sideEffects": false to the package.json, with the following result:

Bundle size with sideEffects false

Which still doesn't solve my problem...

How can I solve this? Why are angular modules that aren't loaded in the AppModule bundled in the main bundle?

EDIT

I created a blank angular app:

Bundle size for blank angular app

Installed my library and added the global styles:

Bundle size with global bootstrap styles

And created a page (ng g module demo --module app --route demo) and added the BsNavbarComponent on it:

Bundle size with bootstrap navbar added on a lazy-loaded page

This immediately increased my main bundle size with 50kB...

Pieterjan
  • 2,738
  • 4
  • 28
  • 55
  • My first thought is that you have imported it somewhere else. – Antoniossss Oct 10 '22 at 09:00
  • Thanks, no I checked it. Each module (except for the `BsNavbarModule`) is only solely imported on the corresponding page (lazy-loaded) in the demo application. The question I linked on top suggests that it's because I'm exposing the modules/components through the library `public-api`. But this actually doesn't make sense. However I tried removing all pages, and removing the `export * from ./xxxx` and even still the bundle size doesn't nearly change... – Pieterjan Oct 10 '22 at 09:12
  • I'm not an expert on bootstrap library, but you must accept that lazy loading is not a magical solution. For example, all services needs to be included in main bundle, regardless of usage. So everything referenced in services should be included too. – Edmunds Folkmanis Oct 10 '22 at 09:52
  • @Pieterjan you have multiple component/module imports in navbar module – Antoniossss Oct 10 '22 at 09:58
  • @Antoniossss I know, the `AppModule` loads the `BsNavbarModule` which loads the `ClickOutsideModule`. But that's really it. But somehow the following modules are also bundled in the main bundle, whilst not loaded on the `AppModule`: `OverlayModule`, `ScrollingModule`, `PortalModule` (from @angular/cdk), `FormsModule`, `HighlightModule`. Then also almost every module from my library is bundled in the main bundle: `BsOffcanvasModule`, `BsSelect2Module`, `BsTimePickerModule`, ... None of these modules are loaded in the `AppModule` – Pieterjan Oct 10 '22 at 11:02
  • @Edmunds Folkmanis, I know that singleton services are included in the main bundle, but there's really a lot of modules (js chunks) that aren't loaded and aren't necessary to be bundled in the main bundle... – Pieterjan Oct 10 '22 at 11:05
  • But arent those used in imported components? I didnt analyze it deeply. – Antoniossss Oct 10 '22 at 11:10
  • No, the `BsNavbarModule` and `ClickOutsideModule` are the only modules that are used, those modules don't load any other modules. That's why it's so puzzling to me... For example, the `BsOffcanvasModule` is loaded only on the corresponding page, but still it's bundled in the main bundle – Pieterjan Oct 10 '22 at 11:17
  • The only reference to the `BsOffcanvasModule` is in the `OffcanvasModule` (page) which is lazy-loaded. So it should not be bundled in the main bundle – Pieterjan Oct 10 '22 at 11:21
  • Created a minimal angular workspace, even without library, and I can see the 2 components, which are seperately used on some pages, being bundled together, for no reason!! – Pieterjan Oct 12 '22 at 12:07
  • By default an angular project uses the [following builder](https://github.com/angular/angular-cli/tree/main/packages/angular_devkit/build_angular/src/builders/browser). Perhaps it would be possible to create a custom builder that properly bundles all the chunks instead of smashing them all together... – Pieterjan Oct 13 '22 at 14:06
  • `::ng-deep` styles are bundled in the main bundle. But shouldn't `:host ::ng-deep` styles be bundled into the component module bundle? – Pieterjan Nov 28 '22 at 07:42
  • [Finally found an excellent resource](https://dev.to/ag-grid/reducing-angular-library-contributions-to-the-main-bundle-3114) - This is literally what I'm experiencing... – Pieterjan Dec 07 '22 at 08:10
  • [Entrypoints and code splitting](https://angular.io/guide/angular-package-format#entrypoints-and-code-splitting) – Pieterjan Dec 08 '22 at 07:31

2 Answers2

3

Problem solved

Documentation

The folder for sub-entrypoints MUST be next to the src folder.

Pieterjan
  • 2,738
  • 4
  • 28
  • 55
  • 3
    Thanks for sharing your insights and the link to the documentation. The key here was the following: > The general rule for APF packages is to use entrypoints for the smallest sets of logically connected code possible. For example, the Angular Material package publishes each logical component or set of components as a separate entrypoint - one for Button, one for Tabs, etc. This allows each Material component to be lazily loaded separately, if desired. So instead of providing your exports at e..g. `@ng-lib/something`, provide it in a fine-granular way, e.g. `@ng-lib/something/button` – Clemens Sum Feb 28 '23 at 12:47
2

Basing this answer off of Edit 2, and not the Old Question.

Why are all these components bundled together into a single bundle? (common.js chunk)

  • One big common bundle with both the BsCarouselModule and BsOffcanvasModule (this is not what I expect it to be)

This behavior is a default optimization technique that you can turn off by specifying commonChunk: false in angular.json. However, this is usually a good idea to leave on, because shared code is de-duplicated by putting code used across multiple bundles into one place (common.js chunk).

"architect": {
  "build": {
    "builder": "@angular-devkit/build-angular:browser",
    "options": {
      ...
      "commonChunk": false,
    ...

I've used your minimal angular workspace to compare ng build --source-map ouput below:

commonChunk: true (default)

ng build output (common.js chunk, faster subsequent lazy loads, smaller lazy chunks) output of ng build

commonChunk: false

ng build output (no common.js chunk, slower lazy loads, larger lazy chunks,) enter image description here

So when does does the common chunk get loaded in the UI?

It depends on your imports. It seems to me that if you import even a single module in your main bundle that ends up in the common chunk (because you also imported that module in lazy modules), the common chunk will be loaded during the initial render which could impact performance.

Basically to prevent loading the common chunk in your initial render, you want to avoid importing the same modules in both main and lazy chunks if at all possible.

This is not an issue with the minimal workspace you provided, but I highly suspect this is the case with your old question.

Network tab (minimal workspace, commonChunk: true) showing that common chunk is not loaded on initial render: network tab showing

commonChunk: false also comes with a small tradeoff in the amount of js that is ultimately sent to the client. You are no longer de-duplicating code. Since your lazy chunks are larger, it takes longer to actually load them; but this can be mitigated by implementing a Preloading Strategy (which you should do with lazy loading anyways, but that's a bit outside the scope of this question).

Chris Newman
  • 3,152
  • 1
  • 16
  • 17
  • Thanks. I tested it, I think it'll be better for me. But could I have the component module chunk in a separate jsbundle, instead of it being concatenated to all pages that use it? Because I expected this to be the gain of the module-per-page setup. As few as possible js duplication + as small as possible bundles (at least not 1 massive `common` bundle with all components in it...) – Pieterjan Oct 12 '22 at 19:15
  • Hi, I'm still wondering why the angular team chose to do it like this... Let's say there's 5 different actual components I'm using (`BsGrid`, `BsDatatable`, `BsCard`, `BsModal`, `BsOffcanvas`) each in their own bundle. Angular will merge them together into 1 `common` bundle. So when I modify the `BsCard` and deploy the app again, all users will have to download the entire `common` bundle again, with tons of components. So on each deploy, the `common` bundle can't be read from the browser cache – Pieterjan Oct 13 '22 at 11:50
  • As a second, if a user visits the `PersonListPage`, the browser needs to download the entire `common` bundle with the components, while only the `BsDatatable` is needed. So instead of simply downloading 2 small bundles, and lazy-loading the others afterwards, the browser is required to download 1 massive `common` bundle while only 1 component is needed. Lazy-loading + module-per-component is useless when the landing page is not '/'. It all seems a little odd to me. – Pieterjan Oct 13 '22 at 11:53
  • With the out of the box setup, I'm not aware of a way to do more fine grained chunking. You might be able to figure out something very advanced with a custom webpack configuration, specifically with the SplitChunksPlugin. Can't help much there though, sorry. – Chris Newman Oct 13 '22 at 14:45
  • I think you make some pretty good points. Comparing the two strategies, one big common chunk and one chunk per module, I think there is an argument to be made for each as the default behavior. Neither are perfect imo, there is probably a happy medium but again that would involve custom webpack configuration. I dont know how much the chunking strategy has been updated since the beginning. Maybe it might change again for the better now that SCAM and standalone components are more mainstream. – Chris Newman Oct 13 '22 at 15:35
  • you might be interested to read this old github issue which talks about first introducing the commonChunk boolean config: https://github.com/angular/angular-cli/issues/7021 – Chris Newman Oct 13 '22 at 15:42
  • Thanks for looking into this. It sure can help. However, i started recreating the workspace step by step, whilee keeping an eye on the bundle chunks using the source-map-explorer, and atm I still have the expected result: only AppModule, NavbarModule and ClickOutsideModule chunks are in the main bundle. So something somewhere is dragging all chunks into the main bundle – Pieterjan Oct 19 '22 at 15:09