angulartypescriptangular-lazyloadingwebpack-bundle-analyzer

Using single component from library causes lazy-loaded modules to end up in main bundle


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:

Main bundle, with no references to library modules

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?

What do we get?

Main bundle contains the NavbarModule

A common bundle with all components

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...


Solution

  • You can follow this approach: The folder for sub-entrypoints MUST be next to the src folder. Addressing this design problem involves structing the project, optimizing module dependency management, and improving lazy loading behavior to enhance performance, maintainability, and modularity.

    Documentation