I recently went through interesting guide from Wes Grimes about dynamically loading modules and components in Angular (link). This approach gives very elastic way of avoiding complicated *nfIf and *ngSwitch statements in templates. The problem occurs, when we have dozens of components to choose from, which have hundreds of injected services with heavy kilobytes of npm dependencies. All of these files need to be packed in one bundle and loaded instantly to browser. It would be nice to postpone loading them until we really need them.

Angular offers lazy loading out-of-the-box for routed modules with following syntax for route definitions.

const routes: Routes = [

{

path: 'lazy-loaded-path',

loadChildren: './path/to/module/some.module#SomeModule'

}

];

Unfortunately this is available only for route paths and doesn’t allow to load e.g. different widgets or form inputs dynamically on single page. We need different way to lazy load our tons of dependencies from node_modules ;)

Cut the branch

Main enemy of lazy loading is tree shaking. Basically it means that compiler goes through every dependency we import to our file, then through all dependencies of that files and so on. Finally every piece of code found in this tree of dependencies lands in final bundle. We need to cut off some branches to make final build smaller, but still need to have some way of referring to them and load them later.

Router achieves this by referring to modules by string with file path and module name. For Typescript this is looks like regular string, but Angular Compiler knows how to handle that and puts that module in separate bundle file and loads it in on demand. Fortunately for us we can reuse these internals of Angular at a bit lower level and tweak for our purposes.

Lazy loading components

As example lets create simple form with one text input component, but to exaggerate importance of lazy loading, that input will have dependency on moment.js. It weights over 300kb minified with locales.

First we create form module, which will handle lazy loading and provide outlet for dynamic component.

LoaderService, with lazy loading and dynamic component creation logic.

Form component with outlet and call to LoaderService.

Compiler have no clue that out fancy string in LoaderService is real path to some existing module, so we need to add it also to angular.json.

{

"projects": {

...

"architect": {

...

"build": {

...

"options": {

"lazyModules": ["src/app/form/form-inputs/form-inputs.module"]

I’m not going through details of this solution, because it is well explained in article I mentioned at beginning. In few words it lazily loads FormInputsModule by file path and puts dynamically TextInputComponent into outlet in FormComponent.

One thing should catch our attention. We are importing reference of TextInputComponent to LoaderService and FormComponent. We have to do that, because componentResolver.resolveComponentFactory() needs it to know what kind of component it is going to build. But that is the reference we want to lazy load so we need to break import tree here.

Stay safe, Type-safe

Having definition of TextInputComponent gives also privilege of knowing its input/output types and we can trust TypeScript that we bind to proper data types. Tree shaking doesn’t care and will pack this component to our main bundle making it heavier and heavier.

Here we can use another way to break dependency tree. Component templates.

This component is simple pass-through to TextInputInternalComponent. To be even more safe, we could use an interface with all relevant fields and implement it by both TextInputs.

Loading by template breaks dependency tree. Now we can implement the real input and import moment.js.

Finally, the lazy loaded FormInputsModule.

The effect

342kb of additional module is loaded on demand

GIF is presented on minified application with AOT build. At beginning there is only loaded base of application, after click module with external dependencies is fetched.

How it works

Let’s explain step by step what is going on here.

Reference to TextInputComponent is added to main bundle of application, because it is dependency of LoaderService and there is nothing we can do about this. We just keep it simple as possible, no logic, no injectors, not styles. Until we run loadTextInput() it is not used for anything, so nothings happens. LoaderService loads FormInputsModule by string reference. This is done in lazy way, whole module is fetched from API and executed. The trick here is that the FormInputsModule imports TextInputComponent, not vice-versa. Core module imports only very simple component, which doesn’t import anything at all, so dependency tree ends here. FormInputsModule declares, compiles and exports TextInputComponent. After this step it becomes available for Injector, we can resolveComponentFactory() on it. Instance of TextInputComponent is created. Internally it shows TextInputInternalComponent with external dependencies and desired logic.

This solution is not 100% pure TypeScript safe. Part of dependencies path goes through components inputs, which are not validating bounded types. However it is not coincidence I named the real input component with Internal postfix. TextInputInternalComponent is not exported from FormInputsModule and should stay private inside that module. Only public API is TextInputComponent, so heavy dependency tree does not leak outside.

What about services?

In case we don’t need any components and just need to lazy load some services, there is another solution.

Again, to not create direct dependency to some heavy service we need to refer to it by something lightweight. Like Injection Tokens and Interfaces or Abstract Classes.

Let’s have a service which returns current date.

It’s better to not reference this service directly due to concrete kilobytes of imports. But we can call its methods by interface.

Now we need to tell Angular somehow, that our service is part of some module, which we want lazy load on demand.

Here comes to play the Injection Token.

Finally we can load everything on CalendarLoaderService.

Loading code is very similar to components case. Again we have moduleReference but this time we call moduleReference.injector<GetDate>(GET_DATE) . Injection Token is some kind of magic key, which refers to something, but it’s the module’s responsibility of what will return. That is defined by special provider.

{

provide: GET_DATE,

useClass: CalendarService

}

Also from TypeScript perspective we have interface which makes us sure all input and return types are correct. This costs us zero kilobytes of additional code in main bundle, because interfaces are removed from compiled code. It’s just type definition, no JavaScript logic at all!