javascriptangulartypescriptwebpacklazy-loading

Create a separate bundle file for each library of my workspace


I have an Angular (version 15.2) workspace, that has an host app, three modules and two libraries.

This structure shows what is being used by what:

host
    moduleA
        library1
    moduleB
        library1
        library2
    moduleC
        library1
        library2

The host lazy-loads the three modules in this way:

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

const routes: Routes = [
  {
    path: "a",
    loadChildren: () => import("a").then(m => m.AModule)
  },
  {
    path: "b",
    loadChildren: () => import("b").then(m => m.BModule)
  },
  {
    path: "c",
    loadChildren: () => import("c").then(m => m.CModule)
  }
];

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

Each of the three modules use library1 and/or library2, as shown in the structure at the beginning.

The lazy loading works as expected, meaning that when I navigate to "localhost:4200/a" I can see from the network that I only get moduleA.js (of course with the name generated by Angular).

Naturally I also see the file common.js that, as I was expecting, contains all the code from library1 used in moduleA, moduleB, and moduleC.

The problem is that this common.js file also contains the code from library2, that is only shared by moduleB and moduleC. This causes the browser to load code that is useless in that moment and invalidates (although not entirely) the purpose of the lazy-loading system.

Therefore I was wondering if among the angular configurations there is something to add or change in order to obtain a different bundle for each library.

Desired result

The desired result is that when navigating to "localhost:4200/a" the browser would download moduleA.js and an library1.js. Then when navigating to "localhost:4200/b", the browser would download moduleB.js, library2.js, but not the library1 that has already been downloaded.

What I've already tried

I have already explored solutions like the angular options vendorChunk: false, commonChunk: false and then tried to use webpack's SplitChunkPlugin via the angular json.

With the SplitChunkPlugin method I was able to obtain a common.js file that contained only the code of the library1, that is used by all of the lazy imported modules, but the code of library2 was repeated in moduleB and moduleC.

This is what my custom webpack config looked like:

module.exports = {
  optimization: {
    splitChunks: {
      cacheGroups: {
        library1: {
          test: /[\\/]projects[\\/]library1[\\/]/,
          name: 'library1',
          chunks: 'all'
        },
        library2: {
          test: /[\\/]projects[\\/]library2[\\/]/,
          name: 'library2',
          chunks: 'all'
        }
      }
    }
  }
};

Solution

  • It turned out that the SplitChunksPlugin was indeed what I needed. The plugin wasn't creating two separate bundles because the size of each library wasn't big enough to create a separate bundle, since Webpack has some default min and max sizes for the output bundles.

    So one way to force Webpack to create separate bundles regardless of their size, is adding the key "enforce: true" to the SplitChunksPlugin options:

    module.exports = {
      optimization: {
        splitChunks: {
          cacheGroups: {
            library1: {
              test: /[\\/]projects[\\/]library1[\\/]/,
              name: 'library1',
              enforce: true,
              chunks: 'all'
            },
            library2: {
              test: /[\\/]projects[\\/]library2[\\/]/,
              name: 'library2',
              enforce: true,
              chunks: 'all'
            }
          }
        }
      }
    };
    

    The enforce key

    Tells webpack to ignore splitChunks.minSize, splitChunks.minChunks, splitChunks.maxAsyncRequests and splitChunks.maxInitialRequests options and always create chunks for this cache group.

    (source).