To reduce the initial load time of a single-page application (SPA), it is often beneficial to defer loading certain parts of the application until they are actually needed. Angular 2 has supported this technique, commonly referred to as lazy loading, since RC 5. This approach leverages the newly introduced modules, which allow an application to be organized into multiple, reusable units.

In this piece, I demonstrate how to work with modules and lazy loading using a sample application that relies on webpack for bundling. The complete example is available at this link. If you wish to build your own example using the techniques described here, you can start with a very basic seed project on the Angular 2 team's GitHub page, which also makes use of webpack.

The Example Application

The sample application presented here provides three menu options through the router: Home, Login, and Flug Buchen.

Beispielanwendung

The first two menu items are available from the moment the application starts. However, the parts of the application tied to the Flug Buchen menu item are loaded on demand, only after the user initially navigates to them:

Flug suchen

To achieve this, the application is broken down into four distinct modules:

Modulstruktur des Beispiels

The AppModule holds the root component, which represents the entire application and is instantiated by Angular 2 during the bootstrapping process. Modules of this kind are therefore known as root modules. It references the HomeModule, which contains the components for the first two menu commands. Furthermore, via the router, it loads the FlugModule into the execution environment the first time the Flug Buchen command is invoked. Both the HomeModule and the FlugModule point to the SharedModule, which houses common components and services.

A Common AuthService in the SharedModule

The SharedModule provides, among other things, an AuthService. This service simulates the user's login and logout, keeping track of the user's name and city. As the listing below illustrates, out of a need for simplicity, this is an implementation for highly honest users:

@Injectable()
export class AuthService {

    isLoggedIn: boolean = false;
    userName: string = null;
    city: string = "Wien";

    login() {
        this.isLoggedIn = true;
        this.userName = "Max";
        this.city = "Graz";
    }

    logout() {
        this.isLoggedIn = false;
        this.userName = null;
        this.city = "Wien";
    }
}

An important detail for the rest of this discussion is that the service defaults the city to Wien for an anonymous user, while the demo user used in the example is associated with Graz.

The SharedModule imports both the CommonModule and the FormsModule. The former contains common directives such as ngIf, ngFor, and ngStyle, and the latter adds support for working with forms:

@NgModule({
    imports: [
        CommonModule,
        FormsModule
    ],
    declarations: [
        OrtValidatorDirective,
        OrtAsyncValidatorDirective,
        DateComponent,
        OrtPipe
    ],
    exports: [
        OrtValidatorDirective,
        OrtAsyncValidatorDirective,
        DateComponent,
        OrtPipe
    ],
    providers: []
})
export class SharedModule {

    static forRoot(): ModuleWithProviders {
        return {
            ngModule: SharedModule,
            providers: [AuthService]
        }
    }
}

The declarations section defines the module's contents, which can include directives, components, and pipes. Under exports, the module lists the items it makes available to other modules.

The providers property is where things become more intriguing. It lists the providers that the module is meant to set up globally.
One might expect to find a provider for the aforementioned AuthService here. However, because the lazily loaded FlugModule also accesses the SharedModule, this is not advisable. Due to how lazy loading is implemented, Angular 2 sets up such services again for lazy-loaded modules! The net result would be two instances of the "singleton" AuthService:

Two "Singletons"

This situation is hardly desirable, as it inevitably leads to inconsistencies. For instance, one instance might be aware of a logged-in user, while the other might assume the user is still unknown. To resolve this issue, the Angular team has provided a pattern (or rather an idiom) to follow. The core idea is to offer the module in two forms: one without providers and one with providers. By definition, a static method named forRoot provides the variant that includes the providers. This method creates an instance of ModuleWithProviders, which groups the module along with its providers. This ModuleWithProviders instance is referenced by the root module, which makes the services available globally just once. All other modules should use the variant without providers:

forRoot

The providers listing genuinely defines global providers, as modules do not receive their own injector. To define providers with a restricted scope, they must be registered at the level of a component.

Defining the HomeModule Feature Module

The HomeModule, which is loaded when the application starts, imports, among other things, the SharedModule discussed in the previous section:

@NgModule({
    imports: [
        CommonModule,
        SharedModule
    ],
    declarations: [
        HomeComponent, 
        LoginComponent
    ],
    exports: [
        HomeComponent, 
        LoginComponent
    ],
    providers: []
})
export class HomeModule {
}

Additionally, it exports a HomeComponent that acts as the landing page and a LoginComponent that uses the AuthService to log users in and out.

Defining the FlugModule Feature Module

The lazily loaded FlugModule comes with its own route configuration. This configuration includes a route for the FlugBuchenComponent, which uses an empty path and thus serves as the module's starting page:

const FLUG_ROUTES =    [{
    path: '',
    component: FlugBuchenComponent,
    canActivate: [AuthGuard],
    children: [
        {
            path: 'flug-suchen', 
            component: FlugSuchenComponent
        },
        {
            path: 'flug-suchen-reactive',
            component: FlugSuchenReactiveComponent
        },
        {
            path: 'passagier-suchen',
            component: PassagierSuchenComponent
        },
        {
            path: 'flug-edit/:id',
            component: FlugEditComponent,
            canDeactivate: [FlugEditGuard]
        }
    ]
}];

This configuration also sets up a number of child routes and references two guards that are not discussed in detail here but are set up via the provider array FLUG_ROUTE_PROVIDERS:

export const FLUG_ROUTE_PROVIDERS = [
    AuthGuard,
    FlugEditGuard
];

Since this route configuration extends the routes of the root component, it must be passed to the static RouterModule.forChild method:

export const FlugRouterModule = RouterModule.forChild(FLUG_ROUTES);

This method creates a configured RouterModule specifically for the FlugModule. Once more, this is a ModuleWithProviders instance.

Additionally, the FlugModule includes a FlugSuchenComponent:

@Component({
    selector: 'flug-suchen',
    template: require('./flug-suchen.component.html'),
    providers: [],
    styles: [require('./flug-suchen.component.css')]
})
export class FlugSuchenComponent {

    public von: string = "";
    public nach: string = "";
    public datum: string = (new Date()).toISOString();

    public selectedFlug: Flug;

    constructor(private flugService: FlugService, private authService: AuthService) {
        this.von = authService.city;
    }

    [...]
}

It has the AuthService from the SharedModule injected and adopts the city of the current user into the von property, suggesting it as the departure airport.

The FlugModule references, among other things, the SharedModule as well as the FlugRouterModule which contains the discussed route configuration:

@NgModule({
    imports: [
        CommonModule, 
        FormsModule, 
        ReactiveFormsModule,
        SharedModule,    
        FlugRouterModule 
    ],
    declarations: [
        FlugBuchenComponent, 
        FlugCardComponent, 
        FlugSuchenComponent, 
        FlugSuchenReactiveComponent, 
        PassagierSuchenComponent, 
        FlugEditComponent
    ],
    providers: [
        FlugService, 
        FLUG_ROUTE_PROVIDERS
    ]
})
export class FlugModule {
}

The module under consideration declares the components used by the route configuration. It also sets up a provider for a FlugService. In addition, it includes the FLUG_ROUTE_PROVIDERS array—which contains the guards—in its providers list.

Routes for the AppModule Root Module

The routes for the AppModule point to the HomeComponent and the LoginComponent. The flug-buchen route does not map to a component. Instead, through loadChildren, it passes a lambda expression that loads the FlugModule when needed:

export const ROUTE_CONFIG: Routes = [
    {
        path: '',
        redirectTo: 'home',
        pathMatch: 'full'
    },
    {
        path: 'home',
        component: HomeComponent
    },
    {
        path: 'login',
        component: LoginComponent
    },
    {
        path: 'flug-buchen',
        loadChildren: () => System.import('./modules/flug/flug.module').then(m => m.FlugModule)
    },
    {
        path: '**',
        redirectTo: 'home'
    }
];

The System.import statement prompts webpack 2 to split the bundle at this precise location. In this manner, a separate bundle is generated for the referenced part of the application, which webpack terminology also calls a chunk. At runtime, System.import loads this chunk on demand.

At the time of writing, webpack 2 was still in the BETA phase, with version 1.x being the current stable release. Version 1 uses the require.ensure method to define additional chunks. This method works in a similar way to System.import but relies on callbacks instead of promises. Since loadChildren expects the provided lambda expression to deliver the loaded module via a promise, calls to require.ensure need to be wrapped in such a promise:

{
    path: 'flug-buchen',
    loadChildren: () => new Promise((resolve) => {
        (require as any).ensure([], (require: any) => {
            resolve(require('./modules/flug/flug.module').FlugModule);
        })
    })
},

Webpack uses calls to require.ensure not only at runtime for loading but also during the bundling process to mark chunk boundaries. To ensure that bundling can clearly identify these boundaries, the application must call require.ensure and the nested require with fixed values. This unfortunately prevents encapsulating this somewhat verbose call in a helper method.

For the sake of completeness, the following listing demonstrates the syntax used when the SystemJS module loader is in play. Here, you simply provide a string with the name of the file containing the module to be loaded and the module name:

{
    path: 'flug-buchen',
    loadChildren: 'app/modules/flug/flug.module#FlugModule'
}

To obtain a configured RouterModule even for this configuration, it must be passed to RouterModule.forRoot:

export const AppRoutesModule = RouterModule.forRoot(ROUTE_CONFIG);

In contrast to forChild, forRoot is used for routes in the root module. The resulting AppRoutesModule is subsequently exported.

By convention, forRoot methods are used ONLY in the root module!

Defining the Root Module AppModule

The AppModule declares the AppComponent as its root component and imports the AppRoutesModule discussed earlier with its route configuration:

@NgModule({
    declarations: [
        AppComponent
    ],
    imports: [
        BrowserModule,
        HttpModule,
        FormsModule,
        ReactiveFormsModule,
        AppRoutesModule,
        HomeModule,
        SharedModule.forRoot()
    ],
    providers: [
        { provide: "BASE_URL", useValue: "http://www.angular.at" }
    ],
    bootstrap: [
        AppComponent 
    ]
})
export class AppModule { 
}

Note that the SharedModule must be included via its static forRoot method, given that this is the root module.

Bundling with Webpack

When bundling with webpack, a separate chunk is now created for the FlugModule. Since no name was assigned, it is an unnamed chunk, which webpack labels with a running number. Webpack 1 starts this numbering at 1; webpack 2 at 0:

>webpack
Hash: a16aebe9bab18c48aa57
Version: webpack 2.1.0-beta.21
Time: 13539ms
        Asset     Size  Chunks             Chunk Names
         0.js   562 kB       0  [emitted]
       app.js   280 kB       1  [emitted]  app
     tests.js  5.74 kB       2  [emitted]  tests
    vendor.js  2.77 MB       3  [emitted]  vendor
     0.js.map   683 kB       0  [emitted]
   app.js.map   268 kB       1  [emitted]  app
 tests.js.map  6.98 kB       2  [emitted]  tests
vendor.js.map  2.78 MB       3  [emitted]  vendor
 [418] ./app spec\.ts$ 160 bytes {2} [built]
 [722] multi tests 28 bytes {2} [built]
 [723] multi vendor 40 bytes {3} [built]
    + 1029 hidden modules

Testing the Lazy Loading

To verify the correct operation of lazy loading, you can monitor the network traffic. The Network tab in Chrome's Developer Tools is well-suited for this purpose. If everything works as intended, the chunk containing the FlugModule (here 0.js) should be requested only upon navigating to the Flug buchen menu option:

Verzögertes Laden über Dev-Tools kontrollieren

Testing the Shared AuthService

To confirm that the AuthService from the SharedModule is instantiated only once, even with lazy loading, you should start by logging in a user:

Nach Login

If, after navigating to the lazily loaded FlugModule, the city of Graz is suggested, you can be confident that only a single global AuthService exists. The reason is that the AuthService simulates a user from Graz after they log in (see the AuthService listing above):

Flug suchen

Probing Global Services

To investigate the problem of duplicate singletons when lazy loading is involved, one could—strictly for experimental purposes—grant FlugModule access to the providers within SharedModule. This is achieved once again through the forRoot method:

@NgModule({                     //            _  _
    imports: [                  //      ___ (~ )( ~)
        CommonModule,           //     /   \_\ \/ /
        RouterModule,           //    |   D_ ]\ \/
        FormsModule,            //    |   D _]/\         ReactiveFormsModule,    //     \___/ / /\         FlugRouterModule,       //          (_ )( _)
        // Alt: SharedModule    //
        SharedModule.forRoot()  // <-- Böse!!!! Nur zum Test!!!
    ],
    declarations: [
        FlugBuchenComponent, 
        FlugCardComponent, 
        FlugSuchenComponent, 
        FlugSuchenReactiveComponent, 
        PassagierSuchenComponent, 
        FlugEditComponent
    ],
    providers: [
        FlugService, 
        FLUG_ROUTE_PROVIDERS 
    ]
})
export class FlugModule {
}

The outcome should reproduce the undesirable scenario depicted in the earlier diagram: FlugModule owns its own AuthService. Since user login takes place via the component inside HomeModule, the AuthService used by FlugModule has no knowledge of that user. It assumes that no login has ever occurred, so it consistently suggests Wien as the departure city when searching for flights. As demonstrated in the initial implementation, this is the default value for unauthenticated users.

Once the test is complete, the expression SharedModule.forRoot() should be reverted back to SharedModule to restore the expected system behavior.

Wrap-Up

Starting with RC 5, an Angular 2 application can be organized into multiple reusable modules. To optimize startup performance, the router supports deferring the loading of these modules until they are needed, a technique known as lazy loading. For this to work, the modules to be loaded on demand must reside in a dedicated bundle. Webpack handles this automatically when the application requests the module via require.ensure. Webpack 2 offers more convenience by supporting the promise-based System.import method instead.

Services that are shared across multiple separately loaded modules demand particular attention. In such scenarios, the service must be registered exclusively at the root module level; otherwise, Angular 2 will instantiate the “singleton” multiple times. Consistently applying the pattern recommended by the Angular 2 team for this purpose aids in identifying and correctly addressing these situations.