Code-splitting in Angular or how to share components between lazy modules
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.
- Why should I care?
- Angular CLI code-splitting under the hood
- Simple Angular Application with lazy modules
- How to share components between lazy modules
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.
- On the other hand, if I split shared features among several shared modules and import only those of them needed in every particular module will it reduce the size of my app? Or Angular does all such optimizations by default?
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 pointsmain
which will result in the chunks with the same names.
- for every dynamically loaded module(by using
import()syntax that conforms to the ECMAScript proposal for dynamic imports)
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
Let’s look at Angular CLI source code and find that configuration section:
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
Let’s go real quick over those of options we saw in Angular CLI configuration above:
chunksvalue can be used to filter modules between sync and async chunks. Its value can be
initialmeans only add files to the chunk if they are imported inside
asyncmeans only add files to the chunk if they are imported inside
minChunkstells webpack to only inject modules in chunk if they are shared between at least 2 chunks(1 by default)
nametells 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.
priorityvalue is used to identify the best-matched chunks when a module falls under many chunk groups.
enforcetells webpack to ignore
maxInitialRequestsoptions and always create chunks for this cache group. There is one small gotcha here: if any of those ignored options are provided at the
cacheGrouplevel then that option will still be used.
testcontrols which modules are selected by this cache group. As we could notice, Angular CLI uses this option to move all
minSizeis 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:
vendorchunk if that module is coming from
defaultchunk 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.
commonchunk 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
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
d are lazy modules, meaning they are imported by using
b components use
ab component in their templates.
d components use
The difference between
cd.module is that
ab.module is imported in
cd.module is imported in
This structure describes exactly the doubts we wanted to demystify. Let’s figure out where
cd modules will be in the final output.
1) SplitChunksPlugin’s algorithm starts with giving each previously created chunks an index.
2) Then it loops over all modules in compilation to fill chunkSetsInGraph
Map . This dictionary shows which chunks share the same code.
1,2 main,polyfill row means that there is at least one module that appears in two chunks:
b modules share the same code from
ab-module so we can also notice the combination
3) Walk through all modules and figure out if it’s possible to create a new chunk for a specific
3a) First of all, webpack determines if a module can be added to specific
cacheGroup by checking the
default test undefined => ok common test undefined => ok vendor test function => false
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
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
ab.module is imported into two lazy chunks. So its combinations are
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
Then plugin filters combinations by
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
ab.module is imported inside async(lazy loaded) chunks. This is exactly what
common cache groups require. This way ab.module is added to two new possible chunks(
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
groupNameis the name of the group(
defaultin our case)
chunkNamesrefers 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
d-d-module means that we have
d.module file in
So having that we used
import('./b/b.module') we get
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.
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.
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 overweights
common (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
default~a-a-module~b-b-module takes precedence over the
common 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
- for dynamically loaded modules
- for shared code with the help of SplitChunksPlugin
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
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
mainchunk. So if you want to get a bad initial load performance then it’s the way to go.
- On the other hand, if we split commonly used code across lazy loaded modules then a new shared chunk will be created and will be loaded only if any of those lazy modules are loaded. This should improve the application initial load. But do it wisely because sometimes it’s better to put small code in one chunk that having the extra request needed for a separate chunk load.
I hope now you should clearly understand the output of Angular CLI and distinguish between entry, dynamic and splitted by using SplitChunksPlugin chunks.