Code-splitting in Angular or how to share components between lazy modules

Alexey Zuev
Jan 21 · 9 min read

This article will give you a better understanding of how Angular split your code into chunks.

If you are scared from Angular CLI output showed above or if you’re curious how that code-splitting actually happens then this post is for you.

Code splitting allows you to split your code into various bundles which can then be loaded on demand. If used correctly, can have a major impact on load time.

Contents

  1. Why should I care?

Why should I care?

Let’s imagine you started a brand new Angular project. You read many resources on how to architect an Angular application, what is the appropriate folder structure and, what is most important, how to keep great startup performance.

You chose Angular CLI and created a modular application with lots of lazy-loaded feature modules. And of course, you created a shared module where you put commonly used directives, pipes, and components.

After a while, you caught yourself thinking that once your new feature module requires some functionality from other feature modules you tend to move this functionality to that single shared module.

The application evolves and soon you noticed that its startup time doesn’t meet your(and, most importantly, your client) expectation.

Now, you’re in doubts…

  • If I put all my pipes, directives and common components in one big shared module and then import it in lazy-loaded modules (where I use only one or two of the imported features) it probably may cause unused-code duplicates in the output files.

Let’s demystify!

Angular CLI code-splitting under the hood

As we all know, the current Angular CLI version uses webpack to perform bundling. But despite that, webpack is also responsible for code-splitting.

So, let’s take a look at how webpack does it.

Webpack 4 introduced SplitChunksPlugin that allows us to define some heuristics to split modules into chunks. Many people complain that this configuration seems mysterious. And at the same time, this is the most interesting part of code splitting.

But before SplitChunksPlugin optimization is applied webpack creates a new chunk:

  • for every entry point

Angular CLI configures the following entry points

main
polyfills
styles

which will result in the chunks with the same names.

Do you remember loadChildren syntax? This is the signal for webpack to create a chunk.


Now let’s move on to the SplitChunksPlugin. It can be enabled inside optimization block of webpack.config.js

Let’s look at Angular CLI source code and find that configuration section:

SplitChunksPlugin configuration in Angular CLI 8

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)

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.

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 fromab-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 thecacheGroup.test property.

ab.module tests

default test undefined => okcommon test undefined => okvendor test function => false

default and common cache group didn’t define thetest property so it should pass it. vendor cache group defines a function where there is a filter to only include modules from thenode_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 theshared module, meaning it is imported only in themain 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)

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

So having that we usedimport('./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.

Structure of big chunk name that shares code across multiple lazy modules

We’re ready to fill chunksInfoMap which knows all about all possible new chunks and also knows which modules it should consist of and related chunks where those modules currently reside.

chunksInfoMap

It’s time to filter possible chunks

SplitChunksPlugin loops over chunksInfoMap’s items in order to find the best matching entry. What does it mean?

default cache group has a priority 10 which overweightscommon (which has only 5). This means that default is the best matching entry and it should be processed first.

Once all other requirements are fulfilled webpack removes all chunk’s modules from other possible chunks in chunksInfoMap dictionary. If there is no module left then the module is deleted

This way default~a-a-module~b-b-module takes precedence over thecommon chunk. The latter is removed since it contains the same list of modules.

Last but not least step is to make some optimizations(like remove duplications) and make sure that all requirements like maxSize are fulfilled.

The entire source code of SplitChunksPlugin can be found here


We’ve discovered that webpack creates chunks in three different ways:

  • for each entry
Angular CLI output by type of chunks

Now let’s go back to our doubts about what is the best way to keep shared code.

How to share components between lazy modules

As we’ve seen in our simple Angular application, webpack created separated chunk for ab.module but included cd.module in the main chunk.

Let’s summarize key takeaways from this post:

  • If we put all shared pipes, directives and common components in one big shared module and then import it everywhere(inside sync and async chunks) then that code will be in our initial main chunk. So if you want to get a bad initial load performance then it’s the way to go.

Conclusion

I hope now you should clearly understand the output of Angular CLI and distinguish between entry, dynamic and splitted by using SplitChunksPlugin chunks.

Happy coding!

Angular In Depth

The place where advanced Angular concepts are explained

Alexey Zuev

Written by

I’m a Frontend Architect at Waveaccess

Angular In Depth

The place where advanced Angular concepts are explained

Welcome to a place where words matter. On Medium, smart voices and original ideas take center stage - with no ads in sight. Watch
Follow all the topics you care about, and we’ll deliver the best stories for you to your homepage and inbox. Explore
Get unlimited access to the best stories on Medium — and support writers while you’re at it. Just $5/month. Upgrade