Lazy loading Angular components without a router

3 Sep 2019

Splitting your Angular app into several smaller bundles can make your site faster by reducing download and execution times. Instead of loading all code upfront it's fetched lazily when needed.

Most guides to lazy loading Angular modules use Angular's RouterModule and the loadChildren property to load code when the user first navigates to a certain page. But that means you can't lazy load code if whether the code is needed doesn't depend on a route change.

This article will explain how to lazy load Angular feature modules independently of the router.

Opting into Angular Ivy

To follow this guide you'll need to opt into Angular's new compilation and rendering pipeline, called Angular Ivy.

For a new project pass the --enable-ivy flag to ng new.

For existing projects, add this next to compilerOptions in tsconfig.app.json:

"angularCompilerOptions": {
  "enableIvy": true
},

Getting set up

Ok, so we've got a project. Now we need a module and a component to lazy load.

ng generate module lazy
ng generate component lazy/my-component

To make my component bundle a little bigger I also installed moment.js with npm install moment --save and then imported it at the top of my-component.component.ts:

import * as moment from "moment"
console.log(moment) // Needed or else moment will be excluded from the bundle

We also want an easy way to get the component from the module, so we'll add a getMyComponent function to lazy.module.ts.

export class LazyModule {
  static getMyComponent() {
    return MyComponentComponent
  }
}

Making lazy loading work

First, let's replace the contents of app.component.html with a button to load the component and a placeholder where we can render the component:

<button (click)="showLazyComponent()">Show lazy loaded component</button>
<app-my-component #componentPlaceholder></app-my-component>

Most of the work needs to be done in app.component.ts.

We use @ViewChild to get a reference to the element that will contain the rendered component.

We'll need a ComponentFactoryResolver instance to create the component later, so we modify the constructor to inject it into the component.

Finally, we've got the showLazyComponent click event handler. We use an ES2015 import statement to load the LazyModule, then generate a factory for the component class and render the component into the placeholder.

Here's what the file looks like after we've made all our changes:

import {
  Component,
  ComponentFactoryResolver,
  ViewContainerRef,
  ViewChild
} from '@angular/core';

@Component({
  selector: 'app-root',
  templateUrl: './app.component.html',
  styleUrls: ['./app.component.css']
})
export class AppComponent {
  @ViewChild('componentPlaceholder', { read: ViewContainerRef, static: true })
  public componentPlaceholder: ViewContainerRef;

  constructor(private resolver: ComponentFactoryResolver) {}

  showLazyComponent() {
    import('./lazy/lazy.module').then(({ LazyModule }) => {
      const MyComponent = LazyModule.getMyComponent();
      const factory = this.resolver.resolveComponentFactory(MyComponent);
      const ref = this.componentPlaceholder.createComponent(factory);
    });
  }
}

Angular now creates a separate bundle called lazy-lazy-modules.js. It's only loaded when the "Show lazy loaded component" button is clicked.

Passing values to the component

Passing values to our component through the template doesn't work, so we need to do it directly using the instance ref.

Suppose our component has an input property called message. Here's how we can pass that value to the instance:

ref.instance.message = "Hello"

Likewise, if we have an output property we can subscribe to the event emitter:

ref.instance.evt.subscribe(console.log);

Maintaining meaningful bundle names for the production build

If we run ng build --prod now the file we split off will have a name like 5-es2015.d49b738d7e72bc78a00f.js. But that doesn't tell us very much about what's being loaded.

If you add the --named-chunks flag to the ng build command you'll get a meaningful chunk name like lazy-lazy-module-es2015.6d182f0f840a62287af1.js instead.

Having consistent bundle names also allows a tool like DebugBear to track the download size of your different bundles over time.

bundle-size-monitoring.png

DebugBear is a website monitoring tool built for front-end developers. Track performance metrics and Lighthouse scores in CI and production. Learn more.

Get new articles on web performance by email.

© 2019 DebugBear Ltd