We will be focusing on cacheGroups options here since this is the “recipe” for webpack on how to create separated chunks based on some conditions.

cacheGroups is a plain object where the key is a group name. Basically, we can think of a cache group as a potential opportunity for a new chunk to be created.

Each group has many configurations and can inherit configuration from splitChunks level.

Let’s go real quick over those of options we saw in Angular CLI configuration above:

chunks value can be used to filter modules between sync and async chunks. Its value can be initial , async or all . initial means only add files to the chunk if they are imported inside sync chunks. async means only add files to the chunk if they are imported inside async chunks( async by default)

value can be used to filter modules between sync and async chunks. Its value can be , or . means only add files to the chunk if they are imported inside chunks. means only add files to the chunk if they are imported inside chunks( by default) minChunks tells webpack to only inject modules in chunk if they are shared between at least 2 chunks(1 by default)

tells webpack to only inject modules in chunk if they are shared between at least 2 chunks(1 by default) name tells webpack to use that name for a newly created chunk. Specifying either a string or a function that always returns the same string will merge all common modules into a single chunk.

tells webpack to use that name for a newly created chunk. Specifying either a string or a function that always returns into a single chunk. priority value is used to identify the best-matched chunks when a module falls under many chunk groups.

value is used to identify the best-matched chunks when a module falls under many chunk groups. enforce tells webpack to ignore minSize , minChunks , maxAsyncRequests and maxInitialRequests options and always create chunks for this cache group. There is one small gotcha here: if any of those ignored options are provided at the cacheGroup level then that option will still be used.

tells webpack to ignore , , and options and always create chunks for this cache group. There is one small gotcha here: if any of those ignored options are provided at the level then that option will still be used. test controls which modules are selected by this cache group. As we could notice, Angular CLI uses this option to move all node_modules dependencies to vendor chunk.

controls which modules are selected by this cache group. As we could notice, Angular CLI uses this option to move all dependencies to chunk. minSize is used to identify minimum size, in bytes, for a chunk to be generated. It didn’t appear in Angular CLI config but it is a very important option that we should be aware of. (As source code states, it’s 30kb by default in production and 10kb in dev environment)

Tip: despite the fact the webpack documentation defines defaults I would refer to webpack source code to find the exact values

Let’s recap here: Angular CLI will move a module to:

vendor chunk if that module is coming from node_modules directory.

chunk if that module is coming from directory. default chunk if that module is imported inside an async module and shared between at least two modules . Note that many default chunks are possible here. I will explain how webpack generates names for those chunks later.

chunk if that module and . Note that I will explain how webpack generates names for those chunks later. common chunk if that module is imported inside an async module and shared between at least two modules and did not fall under default chunk(hello priority ) and also no matter which size it is(thanks to the enforce option)

Enough theory, let’s practice.

Simple Angular application with lazy modules

To explain the process of SplitChunksPlugin, we are going to start with a simplified version of Angular application:

app

├── a(lazy)

│ └── a.component.ts

│ └── a.module.ts

│

├── ab

│ └── ab.component.ts

│ └── ab.module.ts

│

├── b(lazy)

│ └── b.component.ts

│ └── b.module.ts

│

└── c(lazy)

│ └── c.component.ts

│ └── c.module.ts

│

└── cd

│ └── cd.component.ts

│ └── cd.module.ts

│

└── d(lazy)

│ └── d.component.ts

│ └── d.module.ts

│

└── shared

│ └── shared.module.ts

│

└── app.component.ts

└── app.module.ts

Here a , b , c and d are lazy modules, meaning they are imported by using import() syntax.

a and b components use ab component in their templates. c and d components use cd component.

Dependencies between Angular modules

The difference between ab.module and cd.module is that ab.module is imported in a.module and b.module while cd.module is imported in shared.module .

This structure describes exactly the doubts we wanted to demystify. Let’s figure out where ab and cd modules will be in the final output.

Algorithm

1) SplitChunksPlugin’s algorithm starts with giving each previously created chunks an index.

chunks by index

2) Then it loops over all modules in compilation to fill chunkSetsInGraph Map . This dictionary shows which chunks share the same code.

chunkSetsInGraph

E.g. 1,2 main,polyfill row means that there is at least one module that appears in two chunks: main and polyfill .

a and b modules share the same code from ab-module so we can also notice the combination (4,5) above.

3) Walk through all modules and figure out if it’s possible to create a new chunk for a specific cacheGroup .

3a) First of all, webpack determines if a module can be added to specific cacheGroup by checking the cacheGroup.test property.

ab.module tests

default test undefined => ok common test undefined => ok vendor test function => false

default and common cache group didn’t define the test property so it should pass it. vendor cache group defines a function where there is a filter to only include modules from the node_modules path.

cd.module tests are the same.

3b) Now it’s time to walk through all chunk combinations.

Each module understands in which chunks it appears(thanks to module.chunksIterable property).

ab.module is imported into two lazy chunks. So its combinations are (4) , (5) and (4,5) .

On the other hand, cd.module is imported only in the shared module, meaning it is imported only in the main chunk. Its combinations are only (1) .

Then plugin filters combinations by minChunk size:

if (chunkCombination.size < cacheGroup.minChunks) continue;

Since ab.module has the combination (4,5) it should pass this check. This we can not say about cd.module. At this point, this module remains to live inside main chunk.

3c) There is one more check by cacheGroup.chunkds ( initial , async or all )

ab.module is imported inside async(lazy loaded) chunks. This is exactly what default and common cache groups require. This way ab.module is added to two new possible chunks( default and common ).

I promised it earlier so here we go.

How does webpack generate the name for a chunk created by SplitChunksPlugin?

The simplified version of that can be represented as

where:

groupName is the name of the group( default in our case)

is the name of the group( in our case) ~ is a defaultAutomaticNameDelimiter

is a chunkNames refers to the list of all chunk names which are included in that group. That name is like a fullPath path but instead of slash it uses — .

E.g. d-d-module means that we have d.module file in d folder.

So having that we used import('./a/a.module') and import('./b/b.module') we get

Structure of default chunk name

One more thing worth mentioning is that when the length of a chunk name reaches 109 characters, webpack cuts it and adds some hash at the end.